0% found this document useful (0 votes)
24 views372 pages

Vanilla Javascript Book

Another one a Js documents from CHIMANUKA MOÏSE

Uploaded by

moise7cimanuka
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
24 views372 pages

Vanilla Javascript Book

Another one a Js documents from CHIMANUKA MOÏSE

Uploaded by

moise7cimanuka
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 372

Vanilla JavaScript: From Beginner to

Advanced

A Comprehensive Guide to Modern JavaScript


Development

By Manus AI

Table of Contents

Part I: Beginner Concepts

1. Introduction to JavaScript
2. JavaScript Basics
3. Control Structures
4. Functions
5. Arrays and Objects

Part II: Intermediate Concepts

1. Document Object Model (DOM)


2. Events
3. Advanced Objects and Prototypes
4. Closures and Scopes
5. AJAX and Fetch API
6. Intermediate Code Examples

Part III: Advanced Concepts

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

• Appendix A: JavaScript Best Practices


• Appendix B: Debugging Techniques
• Appendix C: Resources for Further Learning

Introduction to the Book


Welcome to "Vanilla JavaScript: From Beginner to Advanced" – a comprehensive guide
designed to take you from the fundamentals of JavaScript to mastering its most
advanced concepts. This book is crafted with a unique approach that emphasizes
practical learning through detailed explanations, numerous code examples, and hands-
on exercises.

Why This Book?


In an era where JavaScript frameworks and libraries dominate web development
discussions, the importance of understanding pure, "vanilla" JavaScript is often
overlooked. Yet, a solid grasp of core JavaScript is essential for several reasons:

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.

2. Performance Optimization: Writing efficient JavaScript requires knowledge of the


language's underlying mechanisms and behaviors.

3. Problem-Solving Flexibility: When frameworks don't provide a solution, falling


back to vanilla JavaScript becomes necessary.

4. Long-Term Relevance: While frameworks come and go, the core language remains
relevant and continues to evolve in a backward-compatible way.

Who This Book Is For


This book is designed for a wide range of readers:

• Beginners who want to start their programming journey with JavaScript


• Intermediate developers looking to solidify their understanding of JavaScript
fundamentals
• Advanced developers seeking to deepen their knowledge of JavaScript's more
complex features
• Framework specialists who want to strengthen their understanding of the
underlying language
• Self-taught programmers who want to fill gaps in their knowledge

How This Book Is Structured


The book is organized into three main parts, each building upon the previous:

Part I: Beginner Concepts

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.

Part II: Intermediate Concepts

Chapters 6-11 explore more complex topics such as DOM manipulation, events,
advanced objects, closures, AJAX, and practical intermediate-level code examples.

Part III: Advanced Concepts

Chapters 12-17 delve into sophisticated JavaScript concepts including design patterns,
modules, promises, modern APIs, performance optimization, and advanced real-world
code examples.

Each chapter includes:

• 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

How to Use This Book


For the best learning experience, I recommend:

1. Read sequentially if you're a beginner, as each chapter builds upon previous


concepts.
2. Code along with the examples. Don't just read—type out the code yourself and
experiment with modifications.

3. Complete the exercises at the end of each chapter to reinforce your


understanding.

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.

The Philosophy Behind This Book


This book embraces several core principles:

• 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

1.1 What is JavaScript?


JavaScript is a high-level, interpreted programming language that is one of the core
technologies of the World Wide Web, alongside HTML and CSS. While HTML structures
your content and CSS styles it, JavaScript brings it to life with interactivity and dynamic
behavior.

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:

• Front-end web development


• Back-end server development (Node.js)
• Mobile application development
• Desktop application development
• Game development
• Internet of Things (IoT) programming

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.

1.2 Brief History of JavaScript


JavaScript has a fascinating history that begins in the early days of the web:

• 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.

• 1997: To standardize the language, JavaScript was submitted to ECMA


International, resulting in the ECMAScript standard (ECMA-262).

• 1999-2005: ECMAScript 3 was widely adopted, but attempts to create ECMAScript 4


failed due to disagreements about the language's direction.
• 2009: ECMAScript 5 (ES5) was released with important new features and became
the standard for many years.

• 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.

1.3 JavaScript vs ECMAScript


Newcomers to JavaScript often wonder about the relationship between JavaScript and
ECMAScript:

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.

JavaScript is the implementation of the ECMAScript specification. It's the actual


programming language that developers use to write code.

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.

1.4 Setting Up Your Development Environment


One of the beautiful aspects of JavaScript is its accessibility. To start coding in
JavaScript, you need very little:

Basic Setup

1. A text editor or IDE:


2. For beginners: Visual Studio Code, Sublime Text, or Atom
3. For advanced users: WebStorm, Visual Studio, or other full IDEs

4. A modern web browser:

5. Google Chrome, Mozilla Firefox, Microsoft Edge, or Safari


6. Each comes with built-in developer tools

That's it! Unlike many programming languages, you don't need to install compilers or
complex development environments to get started.

Creating Your First JavaScript File

1. Open your text editor


2. Create a new file and save it with a .js extension (e.g., script.js )
3. Create an HTML file to include your JavaScript:

<!DOCTYPE html>
<html>
<head>
<title>My First JavaScript Page</title>
</head>
<body>
<h1>Hello, JavaScript!</h1>

<!-- Include JavaScript at the end of the body -->


<script src="script.js"></script>
</body>
</html>

1. Save this as index.html in the same folder as your JavaScript file

Advanced Setup (Optional)

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):

Accessing the DevTools

• Windows/Linux: Press F12 or Ctrl+Shift+I


• macOS: Press Cmd+Option+I
• Or right-click on any webpage and select "Inspect"

Key Features of Browser DevTools

1. Console Panel:
2. Run JavaScript code directly
3. View errors, warnings, and log messages
4. Test ideas and debug code

// Try typing this in the console


console.log("Hello from the console!");

1. Elements Panel:
2. Inspect and modify the HTML and CSS of a page

3. See changes in real-time

4. Sources Panel:

5. View and debug your JavaScript files


6. Set breakpoints to pause execution

7. Step through code line by line

8. Network Panel:

9. Monitor network requests


10. Analyze loading performance

11. Debug API calls

12. Application Panel:

13. Inspect storage (localStorage, cookies, etc.)


14. Manage service workers
15. Analyze cached resources

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.

1.6 Your First JavaScript Program


Let's write our first JavaScript program! We'll create a simple script that displays a
message on the webpage and in the console.

Step 1: Create the HTML File

Create a file named index.html with the following content:

<!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>

<!-- Include JavaScript at the end of the body -->


<script src="script.js"></script>
</body>
</html>

Step 2: Create the JavaScript File

Create a file named script.js in the same folder with the following content:

// This is a comment in JavaScript


// Comments are ignored by the browser

// Print a message to the console


console.log("Hello from JavaScript!");

// Wait for the DOM to be fully loaded


document.addEventListener("DOMContentLoaded", function() {
// Get references to HTML elements
const messageElement = document.getElementById("message");
const changeButton =
document.getElementById("changeButton");

// Add a click event listener to the button


changeButton.addEventListener("click", function() {
// Change the text content of the message element
messageElement.textContent = "You clicked the button!";

// Change the color of the text


messageElement.style.color = "red";

// Log a message to the console


console.log("Button was clicked at: " + new
Date().toLocaleTimeString());
});
});

Step 3: Open the HTML File in a Browser

1. Open your browser


2. Drag and drop the index.html file into the browser window, or use File > Open
File
3. You should see a page with a heading, a message, and a button
4. Open the browser's developer tools and look at the Console panel
5. You should see the message "Hello from JavaScript!"
6. Click the button on the page and observe what happens:
7. The message changes
8. The color changes
9. A new log message appears in the console
Understanding the Code

Let's break down what's happening in our JavaScript file:

1. console.log("Hello from JavaScript!"); - This prints a message to the


browser's console.

2. document.addEventListener("DOMContentLoaded", function()
{ ... }); - This sets up a function to run when the HTML document has been
completely loaded.

3. const messageElement = document.getElementById("message"); - This


finds the HTML element with the ID "message" and stores a reference to it.

4. changeButton.addEventListener("click", function() { ... }); -


This sets up a function to run when the button is clicked.

5. messageElement.textContent = "You clicked the button!"; - This


changes the text inside the message element.

6. messageElement.style.color = "red"; - This changes the CSS color


property of the message.

7. console.log("Button was clicked at: " + new


Date().toLocaleTimeString()); - This logs the current time to the console
when the button is clicked.

This simple program demonstrates several key concepts in JavaScript: - Selecting


elements from the HTML document - Responding to events (page load and button click) -
Manipulating the content and style of HTML elements - Logging information to the
console - Working with dates and times

1.7 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• 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

To reinforce what you've learned in this chapter, try these exercises:

Exercise 1: Console Exploration

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()

Exercise 2: Modify the First Program

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

Exercise 3: Create a New Program

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.

Chapter 2: JavaScript Basics

2.1 Variables and Constants


Variables are fundamental building blocks in any programming language. They allow us
to store and manipulate data in our programs. In JavaScript, there are three ways to
declare variables, each with its own purpose and behavior.

Let

The let keyword was introduced in ES6 (ECMAScript 2015) and is now the preferred
way to declare variables in JavaScript:

let age = 25;


let name = "John";
let isStudent = true;

Key characteristics of let :

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.

console.log(x); // Error: Cannot access 'x' before


initialization
let x = 5;

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";

Key characteristics of const :

1. Block scope: Like let , const variables are block-scoped.

2. No reassignment: Once assigned, you cannot change the value of a const


variable.

const TAX_RATE = 0.07;


TAX_RATE = 0.08; // Error: Assignment to constant variable

1. Must be initialized: You must assign a value when declaring a const .

const DATABASE_NAME; // Error: Missing initializer in const


declaration

1. Reference mutability: For complex types like objects and arrays, the content can
be modified even though the reference cannot change.

const person = { name: "Alice" };


person.name = "Bob"; // This is allowed
person = { name: "Charlie" }; // Error: Assignment to constant
variable

const numbers = [1, 2, 3];


numbers.push(4); // This is allowed
numbers = [5, 6, 7]; // Error: Assignment to constant variable

Var (Legacy)

Before ES6, var was the only way to declare variables in JavaScript:

var message = "Hello";


var count = 42;

Key characteristics of var :

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.

console.log(hoisted); // undefined (not an error)


var hoisted = "I was hoisted";

1. Redeclaration allowed: You can declare the same variable multiple times.

var y = 1;
var y = 2; // This is allowed

When to Use Each

• Use const by default: For variables that should not be reassigned


• Use let when needed: For variables that need to be reassigned
• Avoid var : It's generally better to use let and const for clearer scoping rules
Naming Conventions

Good variable names make your code more readable and maintainable:

1. camelCase: Standard for variables and functions


javascript let firstName = "John"; let totalAmount = 99.95;

2. PascalCase: Used for classes and constructor functions javascript class


UserProfile { } function Person(name) { }

3. UPPER_SNAKE_CASE: Common for constants javascript const


MAX_RETRY_ATTEMPTS = 3; const DEFAULT_API_URL = "https://
api.example.com";

4. Descriptive names: Choose names that explain what the variable contains
```javascript // Bad let x = 86400000;

// Good let millisecondInDay = 86400000; ```

2.2 Data Types


JavaScript is a dynamically typed language, which means you don't need to specify the
data type when declaring a variable. The type is determined automatically when the
program runs.

2.2.1 Primitive Types

JavaScript has seven primitive data types:

1. Number

The number type represents both integer and floating-point numbers:

let integer = 42;


let float = 3.14;
let negative = -10;
let exponent = 2.5e6; // 2,500,000
let binary = 0b1010; // 10 in binary
let octal = 0o744; // 484 in octal
let hex = 0xFF; // 255 in hexadecimal

Special numeric values:


let infinity = Infinity;
let negativeInfinity = -Infinity;
let notANumber = NaN; // Result of invalid mathematical
operations

2. String

The string type represents text data, enclosed in single quotes, double quotes, or
backticks:

let singleQuotes = 'Hello';


let doubleQuotes = "World";
let backticks = `Hello World`;

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:

let name = "Alice";


let age = 30;

// 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 :

let isActive = true;


let isComplete = false;

Booleans are often the result of comparisons:

let isGreater = 5 > 3; // true


let isEqual = 10 === 10; // true
let isNotEqual = "hello" !== "world"; // true

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:

let empty = null;

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

Symbols are guaranteed to be unique:

let sym1 = Symbol("description");


let sym2 = Symbol("description");
console.log(sym1 === sym2); // false

7. BigInt (ES2020)

BigInt allows you to work with integers larger than the Number type can represent:

let bigNumber = 9007199254740991n; // The 'n' suffix creates a


BigInt
let anotherBigNumber = BigInt("9007199254740991");

let result = bigNumber + 1n; // 9007199254740992n

2.2.2 Complex Types

1. Object

Objects are collections of key-value pairs:

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}`;
}
};

Accessing object properties:


// Dot notation
console.log(person.firstName); // "John"
console.log(person.address.city); // "Boston"

// 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"

Adding, modifying, and deleting properties:

// Adding a new property


person.email = "[email protected]";

// Modifying an existing property


person.age = 31;

// Deleting a property
delete person.isEmployed;

2. Array

Arrays are ordered collections of values:

let fruits = ["Apple", "Banana", "Cherry"];


let mixed = [1, "Hello", true, { name: "Object" }, [1, 2, 3]];

Accessing array elements:

console.log(fruits[0]); // "Apple"
console.log(fruits[2]); // "Cherry"
console.log(fruits.length); // 3

Common array methods:

// 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

Functions are objects that can be called to perform actions:

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

// Function expression
const multiply = function(a, b) {
return a * b;
};

// Arrow function (ES6)


const divide = (a, b) => a / b;

// Calling functions
console.log(add(5, 3)); // 8
console.log(multiply(4, 2)); // 8
console.log(divide(10, 2)); // 5

We'll explore functions in much more detail in Chapter 4.

Type Checking

To check the type of a value, you can use the typeof operator:

console.log(typeof 42); // "number"


console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (this is a historical bug
in JavaScript)
console.log(typeof Symbol("id")); // "symbol"
console.log(typeof 42n); // "bigint"
console.log(typeof {}); // "object"
console.log(typeof []); // "object" (arrays are objects in
JavaScript)
console.log(typeof function() {}); // "function"

2.3 Operators
Operators allow you to perform operations on variables and values.

2.3.1 Arithmetic Operators

Arithmetic operators perform mathematical operations:

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)

// Increment and decrement


let x = 5;
x++; // Post-increment: x = 6
++x; // Pre-increment: x = 7
x--; // Post-decrement: x = 6
--x; // Pre-decrement: x = 5

// The difference between post and pre increment/decrement


let y = 5;
let postIncrement = y++; // postIncrement = 5, y = 6
let z = 5;
let preIncrement = ++z; // preIncrement = 6, z = 6

2.3.2 Assignment Operators

Assignment operators assign values to variables:


let x = 10; // Basic assignment

// 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

2.3.3 Comparison Operators

Comparison operators compare values and return a boolean:

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;

console.log(obj1 == obj2); // false (different objects)


console.log(obj1 === obj2); // false (different objects)
console.log(obj1 == obj3); // true (same reference)
console.log(obj1 === obj3); // true (same reference)

2.3.4 Logical Operators

Logical operators perform logical operations and return a boolean:


let x = 5;
let y = 10;

// AND operator (&&)


console.log(x > 0 && y < 20); // true (both conditions are true)
console.log(x > 10 && y < 20); // false (first condition is
false)

// OR operator (||)
console.log(x > 0 || y > 20); // true (first condition is true)
console.log(x > 10 || y > 20); // false (both conditions are
false)

// NOT operator (!)


console.log(!true); // false
console.log(!(x > 10)); // true

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)

// || returns the first truthy value or the last value if all


are falsy
console.log(0 || "Hello"); // "Hello" (first truthy value)
console.log(false || 0 || null); // null (last value, all falsy)

Nullish coalescing operator (??) - ES2020:

// Returns the right-hand operand when the left is null or


undefined
let name = null ?? "Anonymous"; // "Anonymous"
let count = 0 ?? 42; // 0 (0 is not null or undefined)

Other Important Operators

Conditional (Ternary) Operator

let age = 20;


let status = age >= 18 ? "Adult" : "Minor";
console.log(status); // "Adult"

// Nested ternary
let result = age < 13 ? "Child" : age < 18 ? "Teenager" :
"Adult";
console.log(result); // "Adult"

Spread Operator (...)

The spread operator (ES6) expands an iterable into individual elements:

// 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]

// Spreading into function arguments


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

// Spreading objects (ES2018)


let person = { name: "John", age: 30 };
let employee = { ...person, jobTitle: "Developer" }; // { name:
"John", age: 30, jobTitle: "Developer" }

Rest Parameter (...)

The rest parameter (ES6) collects multiple elements into an array:

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

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

Optional Chaining (?.) - ES2020

let user = {
profile: {
// address is missing
}
};
// Without optional chaining
// let city = user.profile.address.city; // Error: Cannot read
property 'city' of undefined

// With optional chaining


let city = user.profile?.address?.city; // undefined (no error)

Destructuring Assignment

Destructuring (ES6) allows you to extract values from arrays or objects:

// 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"

2.4 Type Conversion and Coercion


JavaScript handles type conversions in two ways: explicit conversion (you do it) and
implicit conversion or coercion (JavaScript does it automatically).

Explicit Type Conversion

You can explicitly convert values from one type to another:

String Conversion

// Using String() function


let num = 123;
let str1 = String(num); // "123"

// Using toString() method


let str2 = num.toString(); // "123"
// Template literals
let str3 = `${num}`; // "123"

Number Conversion

// Using Number() function


let str = "123";
let num1 = Number(str); // 123
let bool = true;
let num2 = Number(bool); // 1

// Using parseInt() and parseFloat()


let int = parseInt("123.45"); // 123
let float = parseFloat("123.45"); // 123.45
let hex = parseInt("FF", 16); // 255 (hexadecimal)

// Using + operator
let num3 = +"123"; // 123

Boolean Conversion

// Using Boolean() function


let bool1 = Boolean(1); // true
let bool2 = Boolean(0); // false
let bool3 = Boolean(""); // false
let bool4 = Boolean("hello"); // true
let bool5 = Boolean(null); // false
let bool6 = Boolean(undefined); // false

// Using double negation (!!)


let bool7 = !!"hello"; // true
let bool8 = !!0; // false

Implicit Type Conversion (Coercion)

JavaScript automatically converts types in certain contexts:

String Conversion

// + operator with a string converts the other operand to a


string
let result = "3" + 4; // "34"
let result2 = 1 + 2 + "3"; // "33" (1+2=3, then 3+"3"="33")
Number Conversion

// Arithmetic operators convert operands to numbers


let result = "3" - 2; // 1
let result2 = "3" * "2"; // 6

// Comparison operators also convert to numbers


let comparison = "3" > 2; // true

Boolean Conversion

// Logical operators convert operands to booleans


if ("hello") {
console.log("This will run because non-empty strings are
truthy");
}

if (0) {
console.log("This won't run because 0 is falsy");
}

Truthy and Falsy Values

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

Template literals use backticks (`) instead of quotes:

let simple = `This is a template literal`;

String Interpolation

You can embed expressions using ${expression} syntax:

let name = "Alice";


let greeting = `Hello, ${name}!`; // "Hello, Alice!"

let a = 5;
let b = 10;
let sum = `${a} + ${b} = ${a + b}`; // "5 + 10 = 15"

// You can call functions inside interpolation


function capitalize(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
let message = `${capitalize(name)} is ${a + b} years old`; //
"Alice is 15 years old"

Multi-line Strings

Template literals preserve line breaks:

let multiLine = `This is line one


This is line two
This is line three`;

console.log(multiLine);
// Output:
// This is line one
// This is line two
// This is line three
Tagged Templates

Tagged templates allow you to parse template literals with a function:

function highlight(strings, ...values) {


let result = "";
strings.forEach((string, i) => {
result += string;
if (i < values.length) {
result += `<strong>${values[i]}</strong>`;
}
});
return result;
}

let name = "Alice";


let age = 28;
let html = highlight`My name is ${name} and I am ${age} years
old`;
// "My name is <strong>Alice</strong> and I am <strong>28</
strong> years old"

2.6 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• 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

Understanding these fundamentals is crucial for JavaScript programming. Variables


store data, data types determine how that data behaves, operators manipulate the data,
and type conversion allows you to change data from one form to another.
Exercises

Exercise 1: Variable Declaration and Types

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 2: Type Conversion

1. Convert the string "42" to a number using three different methods.


2. Convert the number 123 to a string using three different methods.
3. Create examples of implicit type conversion with arithmetic and comparison
operators.
4. Create a function that takes any input and returns its boolean equivalent.

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.

Exercise 4: Template Literals

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.

Exercise 5: Practical Application

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.

3.1 Conditional Statements


Conditional statements allow your program to make decisions based on certain
conditions. JavaScript provides several ways to implement conditional logic.

3.1.1 if/else Statements

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:

let temperature = 25;

if (temperature > 20) {


console.log("It's a warm day!");
}

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:

let temperature = 15;

if (temperature > 20) {


console.log("It's a warm day!");
} else {
console.log("It's a cool day!");
}

if/else if/else Statement

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:

let temperature = 15;

if (temperature > 30) {


console.log("It's hot outside!");
} else if (temperature > 20) {
console.log("It's a warm day!");
} else if (temperature > 10) {
console.log("It's a cool day!");
} else {
console.log("It's cold outside!");
}

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:

let temperature = 25;


let isRaining = false;

if (temperature > 20) {


if (!isRaining) {
console.log("It's a nice day for a walk!");
} else {
console.log("It's warm but raining, take an umbrella!");
}
} else {
console.log("It's too cold for a walk.");
}

Best Practices for if/else Statements

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!");
}

1. Avoid assignment in conditions: It can lead to bugs and confusion.

// Confusing and error-prone


if (x = 10) {
// This will always execute because assignment returns the
assigned value
}

// Clear intention
x = 10;
if (x === 10) {
// Code
}

3.1.2 switch Statements

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:

let day = new Date().getDay(); // 0-6, where 0 is Sunday

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:

let day = new Date().getDay();


let typeOfDay;

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}.`);

Switch vs. if/else

When should you use switch instead of if/else ?

• Use switch when comparing a single variable against multiple discrete values
• Use if/else for complex conditions, range checks, or when comparing different
variables

// Good use case for switch


let fruit = "apple";
switch (fruit) {
case "apple": console.log("It's an apple"); break;
case "banana": console.log("It's a banana"); break;
case "orange": console.log("It's an orange"); break;
default: console.log("Unknown fruit");
}

// Better with if/else


let score = 85;
if (score >= 90) {
console.log("Grade: A");
} else if (score >= 80) {
console.log("Grade: B");
} else if (score >= 70) {
console.log("Grade: C");
} else {
console.log("Grade: F");
}

3.1.3 Ternary Operator

The ternary (or conditional) operator provides a shorthand way to write simple if/
else statements. It's particularly useful for simple conditional assignments.
Syntax

condition ? expressionIfTrue : expressionIfFalse

Example:

let age = 20;


let status = age >= 18 ? "Adult" : "Minor";
console.log(status); // "Adult"

This is equivalent to:

let age = 20;


let status;
if (age >= 18) {
status = "Adult";
} else {
status = "Minor";
}
console.log(status); // "Adult"

Nested Ternary Operators

You can nest ternary operators, but be careful with readability:

let age = 15;


let status = age >= 18 ? "Adult" : age >= 13 ? "Teenager" :
"Child";
console.log(status); // "Teenager"

For better readability, you can format nested ternaries with line breaks:

let age = 15;


let status = age >= 18 ? "Adult"
: age >= 13 ? "Teenager"
: "Child";
console.log(status); // "Teenager"

Best Practices for Ternary Operators

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.

let result = (a > b) ? x : y;

1. Consider readability: If a ternary expression becomes too complex, use an if/


else statement instead.

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.

3.2.1 for Loop

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

for (initialization; condition; update) {


// Code to execute in each iteration
}

• initialization: Executed once before the loop starts


• condition: Evaluated before each iteration; if false, the loop stops
• update: Executed after each iteration

Example:

// Count from 1 to 5
for (let i = 1; i <= 5; i++) {
console.log(i);
}
// Output: 1, 2, 3, 4, 5

Looping Through Arrays

The for loop is commonly used to iterate through arrays:


let fruits = ["Apple", "Banana", "Cherry", "Date",
"Elderberry"];

for (let i = 0; i < fruits.length; i++) {


console.log(`Fruit ${i + 1}: ${fruits[i]}`);
}

Multiple Initialization or Update Expressions

You can include multiple expressions in the initialization and update parts of a for loop
by separating them with commas:

// Count from 1 to 5, tracking both the number and its square


for (let i = 1, square = 1; i <= 5; i++, square = i * i) {
console.log(`${i} squared is ${square}`);
}

Optional Parts

All three parts of the for loop are optional:

// Initialization outside the loop


let i = 1;
for (; i <= 5; i++) {
console.log(i);
}

// Update inside the loop


for (let j = 1; j <= 5;) {
console.log(j);
j++;
}

// Infinite loop (be careful!)


let k = 1;
for (;;) {
console.log(k);
k++;
if (k > 5) break; // Exit condition
}

3.2.2 while Loop

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;

while (count <= 5) {


console.log(count);
count++;
}
// Output: 1, 2, 3, 4, 5

Use Cases for while Loops

while loops are particularly useful for:

1. Reading data until a certain condition is met:

let userInput = "";


while (userInput !== "quit") {
// In a browser environment, you might use prompt()
// userInput = prompt("Enter a command (type 'quit' to
exit):");
console.log(`You entered: ${userInput}`);

// For this example, let's simulate user input


if (Math.random() < 0.2) userInput = "quit";
else userInput = ["help", "list", "add", "delete"]
[Math.floor(Math.random() * 4)];
}

1. Processing data with an unknown number of iterations:

function findFactors(num) {
let factor = 2;
let factors = [];

while (num > 1) {


if (num % factor === 0) {
factors.push(factor);
num /= factor;
} else {
factor++;
}
}

return factors;
}

console.log(findFactors(60)); // [2, 2, 3, 5]

3.2.3 do...while Loop

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

When to Use do...while

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:");

// For this example, let's simulate user input


answer = Math.floor(Math.random() * 20);
console.log(`User entered: ${answer}`);
} while (answer <= 10);

console.log(`Thank you! You entered ${answer}, which is greater


than 10.`);

3.2.4 for...in Loop

The for...in loop iterates over all enumerable properties of an object. It's primarily
designed for objects, not arrays.

Syntax

for (let key in object) {


// Code to execute for each property
}

Example with an object:

let person = {
firstName: "John",
lastName: "Doe",
age: 30,
occupation: "Developer"
};

for (let property in person) {


console.log(`${property}: ${person[property]}`);
}
// Output:
// firstName: John
// lastName: Doe
// age: 30
// occupation: Developer

Caution with Arrays

While you can use for...in with arrays, it's generally not recommended because:

1. The order of iteration is not guaranteed


2. It iterates over all enumerable properties, not just array indices
3. It's slower than other loop types for arrays

let numbers = [10, 20, 30];


numbers.customProperty = "This is a custom property";
for (let key in numbers) {
console.log(`${key}: ${numbers[key]}`);
}
// Output:
// 0: 10
// 1: 20
// 2: 30
// customProperty: This is a custom property

3.2.5 for...of Loop

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

for (let value of iterable) {


// Code to execute for each value
}

Example with an array:

let fruits = ["Apple", "Banana", "Cherry"];

for (let fruit of fruits) {


console.log(fruit);
}
// Output:
// Apple
// Banana
// Cherry

Example with a string:

let greeting = "Hello";

for (let character of greeting) {


console.log(character);
}
// Output:
// H
// e
// l
// l
// o

for...of vs. forEach

Arrays also have a forEach method, which is similar to for...of but with some
differences:

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

// Using for...of
for (let num of numbers) {
console.log(num);
}

// Using forEach
numbers.forEach(function(num) {
console.log(num);
});

// Using forEach with arrow function


numbers.forEach(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

numbers.forEach((num, index, array) => {


console.log(`numbers[${index}] = ${num}`);
// We can access the original array if needed
if (index === array.length - 1) {
console.log("This is the last element");
}
});

3.3 Break and Continue


The break and continue statements provide additional control within loops.

The break Statement

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

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

for (let i = 1; i <= 10; i++) {


if (i % 2 === 0) {
continue; // Skip even numbers
}
console.log(i);
}
// Output: 1, 3, 5, 7, 9

Labeled Statements

Labels can be used with break and continue to specify which loop to break from or
continue in nested loops:

outerLoop: for (let i = 1; i <= 3; i++) {


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

With continue :

outerLoop: for (let i = 1; i <= 3; i++) {


for (let j = 1; j <= 3; j++) {
if (i === 2 && j === 2) {
continue
outerLoop; // Skip to the next iteration of the outer loop
}
console.log(`i = ${i}, j = ${j}`);
}
}
// Output:
// i = 1, j = 1
// i = 1, j = 2
// i = 1, j = 3
// i = 2, j = 1
// i = 3, j = 1
// i = 3, j = 2
// i = 3, j = 3

3.4 Chapter Summary and Exercises

Summary

In this chapter, we've covered JavaScript's control structures:

• Conditional Statements:
• if/else statements for basic decision making
• switch statements for comparing a single value against multiple cases

• Ternary operator for concise conditional expressions

• Loops:

• for loops for iterating a specific number of times


• while loops for iterating as long as a condition is true
• do...while loops for iterating at least once and then as long as a condition is
true
• for...in loops for iterating over object properties

• for...of loops for iterating over iterable values

• Control Flow:

• break statement for exiting loops


• continue statement for skipping iterations
• Labeled statements for controlling nested loops
These control structures are essential tools for creating dynamic and responsive
JavaScript programs. They allow you to create logic that can adapt to different
conditions and process data efficiently.

Exercises

Exercise 1: Conditional Statements

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.

2. Rewrite the following if/else statement using a ternary operator: javascript


let age = 20; let message; if (age >= 18) { message = "You can
vote!"; } else { message = "You cannot vote yet."; }

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.)

4. Use a for...of loop to find the longest string in an array of strings.

Exercise 3: Nested Control Structures

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.

4.1 Function Declarations


A function declaration (also called a function statement) defines a named function that
can be called later in your code.

Basic Syntax

function functionName(parameter1, parameter2, ...) {


// Function body - code to be executed
return value; // Optional return statement
}

Example:

function greet(name) {
return `Hello, ${name}!`;
}

// Calling the function


console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Bob")); // "Hello, Bob!"
Function Hoisting

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:

// We can call the function before declaring it


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

// 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 Without Return Values

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
}

let result = logMessage("This is a message");


console.log(result); // undefined

Functions with Multiple Return Statements

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"

4.2 Function Expressions


A function expression defines a function as part of a larger expression, typically a
variable assignment.

Basic Syntax

const functionName = function(parameter1, parameter2, ...) {


// Function body
return value; // Optional
};

Example:

const multiply = function(a, b) {


return a * b;
};

console.log(multiply(4, 5)); // 20

Anonymous Function Expressions

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

Named Function Expressions

You can also give a name to a function expression:

const factorial = function calculateFactorial(n) {


if (n <= 1) return 1;
return n * calculateFactorial(n - 1);
};

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 .

Function Expressions vs. Function Declarations

Key differences between function expressions and function declarations:

1. Hoisting: Function declarations are hoisted, function expressions are not.

// This works
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}

// This throws an error


console.log(multiply(2, 3)); // Error: multiply is not a
function
const multiply = function(a, b) {
return a * b;
};

1. Usage: Function expressions can be used in places where function declarations


cannot, such as:
2. As arguments to other functions
3. As part of object literals
4. As return values from other functions
5. In immediately invoked function expressions (IIFEs)

// Function expression as an argument


setTimeout(function() {
console.log("This runs after 1 second");
}, 1000);

// Function expression in an object literal


const calculator = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
}
};

// Function expression as a return value


function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10

4.3 Arrow Functions


Arrow functions (introduced in ES6) provide a more concise syntax for writing function
expressions.

Basic Syntax

const functionName = (parameter1, parameter2, ...) => {


// Function body
return value;
};

Example:

const add = (a, b) => {


return a + b;
};

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:

const add = (a, b) => a + b;


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

const square = x => x * x;


console.log(square(4)); // 16

Parameter Handling

Arrow functions handle parameters in the following ways:

1. No parameters: Use empty parentheses

const sayHello = () => "Hello, world!";

1. Single parameter: Parentheses are optional

const double = x => x * 2;


// or
const double = (x) => x * 2;

1. Multiple parameters: Parentheses are required

const add = (a, b) => a + b;

Object Literals

When returning an object literal directly, wrap it in parentheses to avoid confusion with
the function body:

// Without parentheses - Error


// const createPerson = (name, age) => { name: name, age: age };

// With parentheses - Correct


const createPerson = (name, age) => ({ name: name, age: age });

console.log(createPerson("Alice",
30)); // { name: "Alice", age: 30 }
Key Differences from Regular Functions

Arrow functions differ from regular functions in several important ways:

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);
}

const arrowFunction = () => {


// console.log(arguments); // Error: arguments is not
defined
};

regularFunction(1, 2, 3); // [1, 2, 3]

1. Cannot be used as constructors: Arrow functions cannot be used with the new
keyword.

function RegularFunction() {
this.value = 42;
}

const ArrowFunction = () => {


this.value = 42;
};

const instance1 = new RegularFunction(); // Works


// const instance2 = new ArrowFunction(); // Error:
ArrowFunction is not a constructor

1. No super or new.target : Arrow functions don't have their own super or


new.target bindings.

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 .

When to Use Arrow Functions

Arrow functions are best used for:

• Short, simple functions


• Callbacks where you don't need this to refer to the calling object
• Functions that don't need to be constructors
• When you want to preserve the lexical this context

// Good use case: Array methods with callbacks


const numbers = [1, 2, 3, 4, 5];
const squared = numbers.map(x => x * x);
console.log(squared); // [1, 4, 9, 16, 25]

// Good use case: Preserving this context


const counter = {
count: 0,
start: function() {
setInterval(() => {
this.count++; // 'this' refers to the counter object
console.log(this.count);
}, 1000);
}
};

4.4 Parameters and Arguments


Parameters are the variables listed in the function definition, while arguments are the
actual values passed to the function when it's called.

Basic Parameter Usage

function greet(name, greeting) {


return `${greeting}, ${name}!`;
}
console.log(greet("Alice", "Hello")); // "Hello, Alice!"
console.log(greet("Bob", "Hi")); // "Hi, Bob!"

Parameter vs. Argument

It's important to understand the distinction:

// name and greeting are parameters


function greet(name, greeting) {
return `${greeting}, ${name}!`;
}

// "Alice" and "Hello" are arguments


greet("Alice", "Hello");

Missing Arguments

If you call a function with fewer arguments than parameters, the missing parameters are
set to undefined :

function greet(name, greeting) {


return `${greeting}, ${name}!`;
}

console.log(greet("Alice")); // "undefined, Alice!"

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;
}

console.log(add(1, 2, 3, 4)); // 3 (only uses the first two


arguments)
The arguments Object

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

Note that arrow functions don't have the arguments object.

4.5 Default Parameters


Default parameters (introduced in ES6) allow you to specify default values for
parameters that are not provided or are undefined .

Basic Syntax

function functionName(parameter1 = defaultValue1, parameter2 =


defaultValue2, ...) {
// Function body
}

Example:

function greet(name = "Guest", greeting = "Hello") {


return `${greeting}, ${name}!`;
}

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


console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Bob", "Hi")); // "Hi, Bob!"
console.log(greet(undefined, "Hey")); // "Hey, Guest!"

Expressions as Default Values

Default values can be expressions, including function calls:


function getDefaultGreeting() {
return "Welcome";
}

function greet(name = "Guest", greeting = getDefaultGreeting())


{
return `${greeting}, ${name}!`;
}

console.log(greet()); // "Welcome, Guest!"

Using Previous Parameters

Default parameters can use the values of previously defined parameters:

function createUser(name, role = "user", permissions =


getPermissions(role)) {
function getPermissions(role) {
return role === "admin" ? ["read", "write", "delete"] :
["read"];
}

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"] }

4.6 Rest Parameters


Rest parameters (introduced in ES6) allow a function to accept an indefinite number of
arguments as an array.

Basic Syntax

function functionName(param1, param2, ...restParams) {


// restParams is an array containing the rest of the
arguments
}

Example:

function sum(first, ...numbers) {


console.log("First number:", first);

let total = first;


for (let number of numbers) {
total += number;
}
return total;
}

console.log(sum(1, 2, 3, 4, 5)); // First number: 1, Result: 15

Rest Parameters vs. arguments Object

Rest parameters have several advantages over the arguments object:

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:

function example(a, b, ...rest) {


console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]
}

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

1. A function can have only one rest parameter.


2. The rest parameter must be the last parameter in the function definition.

// Valid
function example(a, b, ...rest) { }

// Invalid - rest parameter is not last


// function invalid(...rest, a, b) { }

// Invalid - multiple rest parameters


// function invalid(...rest1, ...rest2) { }

4.7 Return Values


The return statement ends function execution and specifies a value to be returned to
the function caller.

Basic Return

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

let sum = add(5, 3);


console.log(sum); // 8

Multiple Return Values

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];
}

const [x, y] = getCoordinates();


console.log(x, y); // 10 20

// Using an object
function getUserInfo() {
return {
name: "Alice",
age: 30,
isAdmin: false
};
}

const { name, age, isAdmin } = getUserInfo();


console.log(name, age, isAdmin); // "Alice" 30 false

Conditional Returns

Functions can have different return values based on conditions:

function getDiscount(price, memberType) {


if (memberType === "premium") {
return price * 0.2; // 20% discount
} else if (memberType === "standard") {
return price * 0.1; // 10% discount
} else {
return 0; // No discount
}
}

console.log(getDiscount(100, "premium")); // 20
console.log(getDiscount(100, "standard")); // 10
console.log(getDiscount(100, "none")); // 0

Return in Arrow Functions

For arrow functions with a concise body (no curly braces), the expression is implicitly
returned:

const add = (a, b) => a + b;


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

// 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 in Function Scope

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

Nested Functions and Scope Chain

Functions can be nested inside other functions, creating a scope chain:

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;
}

const innerFunc = outer();


innerFunc(); // Still has access to outerVar even though outer()
has finished executing

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:

const value = "global";

function example() {
const value = "local";
console.log(value); // "local"
}

example();
console.log(value); // "global"

Best Practices for Function Scope

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.

Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• Function Declarations: The basic way to define named functions


• Function Expressions: Defining functions as part of expressions
• Arrow Functions: A concise syntax for function expressions with special behavior
• Parameters and Arguments: How to pass data to functions
• Default Parameters: Providing fallback values for parameters
• Rest Parameters: Handling an indefinite number of arguments
• Return Values: Sending data back from functions
• Function Scope: How variables are accessible within functions

Functions are a cornerstone of JavaScript programming. They allow you to write


modular, reusable code that's easier to maintain and understand. As you progress in
your JavaScript journey, you'll find yourself using functions in increasingly sophisticated
ways.

Exercises

Exercise 1: Function Declarations and Expressions

1. Write a function declaration called calculateArea that calculates the area of a


rectangle (width × height).

2. Convert your calculateArea function to a function expression.

3. Convert your function expression to an arrow function.

4. Create a function that calculates the area of a circle (π × radius²). Use Math.PI
for the value of π.

Exercise 2: Parameters and Arguments

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.

3. Create a function with default parameters that generates a personalized greeting


message.

Exercise 3: Return Values

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).

3. Write a function that returns another function (a function factory).

Exercise 4: Function Scope

1. Create a nested function where the inner function uses a variable from the outer
function.

2. Demonstrate variable shadowing by creating a function that has a parameter with


the same name as a global variable.

3. Create a counter function that uses closure to maintain its state.

Exercise 5: Practical Application

Create a simple calculator library:

1. Create an object called calculator with methods for basic operations (add,
subtract, multiply, divide).

2. Add a method that can take a variable number of arguments.

3. Add a method that can chain operations (e.g.,


calculator.add(5).multiply(2).value() ).

4. Include proper error handling (e.g., division by zero).

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.

5.1.1 Creating Arrays

There are several ways to create arrays in JavaScript:

Array Literal Notation

The most common way to create an array is using square brackets [] :

// Empty array
let emptyArray = [];

// Array with values


let fruits = ["Apple", "Banana", "Cherry"];

// Array with mixed data types


let mixed = [42, "Hello", true, null, { name: "John" }, [1, 2,
3]];

Array Constructor

You can also use the Array() constructor:

// Empty array
let emptyArray = new Array();

// Array with predefined length (all elements are undefined)


let arrayWithLength = new Array(5);
console.log(arrayWithLength.length); // 5
console.log(arrayWithLength); // [empty × 5]
// Array with values
let fruits = new Array("Apple", "Banana", "Cherry");

The Array() constructor behaves differently based on its arguments: - With no


arguments: Creates an empty array - With one numeric argument: Creates an array with
that length (all elements undefined) - With multiple arguments or one non-numeric
argument: Creates an array with those elements

Array.of() Method (ES6)

The Array.of() method creates a new array with the provided arguments as
elements, regardless of the number or types of arguments:

// Create array with a single number


let numbers = Array.of(5);
console.log(numbers); // [5] (not an array of length 5)

// Create array with multiple values


let mixed = Array.of(42, "Hello", true);
console.log(mixed); // [42, "Hello", true]

Array.from() Method (ES6)

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]

// With a mapping function


let numbers = Array.from([1, 2, 3], x => x * 2);
console.log(numbers); // [2, 4, 6]

5.1.2 Accessing Array Elements

You can access array elements using their index (position), which starts at 0:

let fruits = ["Apple", "Banana", "Cherry", "Date"];

// Accessing by index
console.log(fruits[0]); // "Apple"
console.log(fruits[2]); // "Cherry"

// Using the last index


console.log(fruits[fruits.length - 1]); // "Date"

Negative Indices with at() Method (ES2022)

The at() method allows you to use negative indices to access elements from the end of
the array:

let fruits = ["Apple", "Banana", "Cherry", "Date"];

// Using at() with positive index


console.log(fruits.at(0)); // "Apple"

// Using at() with negative index


console.log(fruits.at(-1)); // "Date" (last element)
console.log(fruits.at(-2)); // "Cherry" (second-to-last element)

Out of Bounds Access

If you try to access an element at an index that doesn't exist, JavaScript returns
undefined :

let fruits = ["Apple", "Banana", "Cherry"];


console.log(fruits[5]); // undefined

5.1.3 Array Methods

JavaScript arrays come with many built-in methods that make them powerful and
flexible.

Adding and Removing Elements

Adding elements to the end:

let fruits = ["Apple", "Banana"];

// 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"]

Adding elements to the beginning:


let fruits = ["Banana", "Cherry"];

// unshift() adds elements to the beginning and returns the new


length
let newLength = fruits.unshift("Apple");
console.log(newLength); // 3
console.log(fruits); // ["Apple", "Banana", "Cherry"]

Removing elements from the end:

let fruits = ["Apple", "Banana", "Cherry"];

// pop() removes the last element and returns it


let lastFruit = fruits.pop();
console.log(lastFruit); // "Cherry"
console.log(fruits); // ["Apple", "Banana"]

Removing elements from the beginning:

let fruits = ["Apple", "Banana", "Cherry"];

// shift() removes the first element and returns it


let firstFruit = fruits.shift();
console.log(firstFruit); // "Apple"
console.log(fruits); // ["Banana", "Cherry"]

Modifying Arrays

Changing elements:

let fruits = ["Apple", "Banana", "Cherry"];

// Change an element by its index


fruits[1] = "Blueberry";
console.log(fruits); // ["Apple", "Blueberry", "Cherry"]

Splicing arrays:

The splice() method changes the contents of an array by removing or replacing


existing elements and/or adding new elements:

let fruits = ["Apple", "Banana", "Cherry", "Date"];

// 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"]

// Remove and add elements


fruits = ["Apple", "Banana", "Cherry", "Date"];
removed = fruits.splice(1, 2, "Blueberry", "Blackberry");
console.log(removed); // ["Banana", "Cherry"]
console.log(fruits); // ["Apple", "Blueberry", "Blackberry",
"Date"]

// Add elements without removing any


fruits = ["Apple", "Banana"];
fruits.splice(1, 0, "Apricot", "Avocado");
console.log(fruits); // ["Apple", "Apricot", "Avocado",
"Banana"]

Slicing arrays:

The slice() method returns a shallow copy of a portion of an array without modifying
the original array:

let fruits = ["Apple", "Banana", "Cherry", "Date",


"Elderberry"];

// slice(start, end) - end is exclusive


let citrus = fruits.slice(1, 4);
console.log(citrus); // ["Banana", "Cherry", "Date"]
console.log(fruits); // Original array is unchanged

// Omitting end slices to the end of the array


let lastTwo = fruits.slice(3);
console.log(lastTwo); // ["Date", "Elderberry"]

// Negative indices count from the end


let lastThree = fruits.slice(-3);
console.log(lastThree); // ["Cherry", "Date", "Elderberry"]

Searching and Filtering

Finding elements by value:

let fruits = ["Apple", "Banana", "Cherry", "Date", "Banana"];

// indexOf() returns the first index at which a given element


can be found
let bananaIndex = fruits.indexOf("Banana");
console.log(bananaIndex); // 1

// lastIndexOf() returns the last index at which a given element


can be found
let lastBananaIndex = fruits.lastIndexOf("Banana");
console.log(lastBananaIndex); // 4

// includes() determines whether an array includes a certain


value
let hasCherry = fruits.includes("Cherry");
console.log(hasCherry); // true

Finding elements by condition:

let people = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 35 },
{ name: "David", age: 30 }
];

// find() returns the first element that satisfies the provided


testing function
let person = people.find(person => person.age === 30);
console.log(person); // { name: "Bob", age: 30 }

// findIndex() returns the index of the first element that


satisfies the provided testing function
let personIndex = people.findIndex(person => person.age === 30);
console.log(personIndex); // 1

// 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:

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

// map() creates a new array with the results of calling a


function on every element
let squared = numbers.map(num => num * num);
console.log(squared); // [1, 4, 9, 16, 25]
let people = [
{ firstName: "John", lastName: "Doe" },
{ firstName: "Jane", lastName: "Smith" }
];

let fullNames = people.map(person => `${person.firstName} $


{person.lastName}`);
console.log(fullNames); // ["John Doe", "Jane Smith"]

Reducing:

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

// reduce() applies a function against an accumulator and each


element to reduce it to a single value
let sum = numbers.reduce((accumulator, currentValue) =>
accumulator + currentValue, 0);
console.log(sum); // 15

// Finding the maximum value


let max = numbers.reduce((max, current) => Math.max(max,
current), -Infinity);
console.log(max); // 5

// 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:

let nestedArrays = [[1, 2], [3, 4], [5, 6]];

// flat() creates a new array with all sub-array elements


concatenated recursively
let flattened = nestedArrays.flat();
console.log(flattened); // [1, 2, 3, 4, 5, 6]

// Deep nesting with depth parameter


let deeplyNested = [1, [2, [3, [4]]]];
console.log(deeplyNested.flat()); // [1, 2, [3, [4]]]
console.log(deeplyNested.flat(2)); // [1, 2, 3, [4]]
console.log(deeplyNested.flat(Infinity)); // [1, 2, 3, 4]

// flatMap() maps each element using a mapping function, then


flattens
let sentences = ["Hello world", "How are you"];
let words = sentences.flatMap(sentence => sentence.split(" "));
console.log(words); // ["Hello", "world", "How", "are", "you"]

Sorting and Reversing

Sorting arrays:

let fruits = ["Cherry", "Apple", "Banana", "Date"];

// sort() sorts the elements of an array in place


fruits.sort();
console.log(fruits); // ["Apple", "Banana", "Cherry", "Date"]

// Sorting numbers (be careful, default sort converts to


strings)
let numbers = [10, 5, 40, 25, 100];
numbers.sort();
console.log(numbers); // [10, 100, 25, 40, 5] (incorrect numeric
sort)

// Correct numeric sort with compare function


numbers.sort((a, b) => a - b);
console.log(numbers); // [5, 10, 25, 40, 100]

// Sorting objects
let people = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 20 }
];

people.sort((a, b) => a.age - b.age);


console.log(people); // Sorted by age ascending

Reversing arrays:

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

// reverse() reverses the elements of an array in place


numbers.reverse();
console.log(numbers); // [5, 4, 3, 2, 1]

Other Useful Methods

Joining arrays:
let fruits = ["Apple", "Banana", "Cherry"];

// join() joins all elements of an array into a string


let list = fruits.join(", ");
console.log(list); // "Apple, Banana, Cherry"

// Default separator is comma


let defaultJoin = fruits.join();
console.log(defaultJoin); // "Apple,Banana,Cherry"

Filling arrays:

// fill() fills all the elements with a static value


let zeros = new Array(5).fill(0);
console.log(zeros); // [0, 0, 0, 0, 0]

// Fill a portion of an array


let numbers = [1, 2, 3, 4, 5];
numbers.fill(0, 2, 4); // Fill with 0 from index 2 to 4
(exclusive)
console.log(numbers); // [1, 2, 0, 0, 5]

Testing arrays:

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

// every() tests if all elements pass the test


let allPositive = numbers.every(num => num > 0);
console.log(allPositive); // true

// some() tests if at least one element passes the test


let hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // true

5.1.4 Array Iteration

There are multiple ways to iterate over arrays in JavaScript:

for Loop

The traditional for loop:

let fruits = ["Apple", "Banana", "Cherry", "Date"];

for (let i = 0; i < fruits.length; i++) {


console.log(`Fruit ${i + 1}: ${fruits[i]}`);
}

for...of Loop (ES6)

The for...of loop provides a cleaner syntax for iteration:

let fruits = ["Apple", "Banana", "Cherry", "Date"];

for (let fruit of fruits) {


console.log(fruit);
}

forEach Method

The forEach method executes a provided function once for each array element:

let fruits = ["Apple", "Banana", "Cherry", "Date"];

fruits.forEach((fruit, index) => {


console.log(`Fruit ${index + 1}: ${fruit}`);
});

Entries, Keys, and Values (ES6)

These methods return iterators for different aspects of the array:

let fruits = ["Apple", "Banana", "Cherry"];

// entries() returns [index, element] pairs


for (let [index, fruit] of fruits.entries()) {
console.log(`${index}: ${fruit}`);
}

// keys() returns indices


for (let index of fruits.keys()) {
console.log(index);
}

// values() returns elements


for (let fruit of fruits.values()) {
console.log(fruit);
}
Array Destructuring (ES6)

Destructuring allows you to extract values from arrays into distinct variables:

let coordinates = [10, 20, 30];

// 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 (ES6)

Typed arrays are array-like objects that provide a mechanism for accessing raw binary
data:

// Int8Array: 8-bit signed integers


let int8 = new Int8Array([127, -128, 0]);
console.log(int8); // Int8Array(3) [127, -128, 0]

// Uint8Array: 8-bit unsigned integers


let uint8 = new Uint8Array([255, 0, 128]);
console.log(uint8); // Uint8Array(3) [255, 0, 128]

// Float32Array: 32-bit floating point numbers


let float32 = new Float32Array([1.5, 2.5, 3.5]);
console.log(float32); // Float32Array(3) [1.5, 2.5, 3.5]
Typed arrays are particularly useful for: - Working with binary data - Processing large
amounts of numeric data efficiently - WebGL and other graphics operations - Audio/
video processing - Network protocols

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.

5.2.1 Creating Objects

There are several ways to create objects in JavaScript:

Object Literal Notation

The most common way to create an object is using curly braces {} :

// Empty object
let emptyObject = {};

// Object with properties


let person = {
firstName: "John",
lastName: "Doe",
age: 30,
isEmployed: true,
skills: ["JavaScript", "HTML", "CSS"],
address: {
street: "123 Main St",
city: "Boston",
country: "USA"
}
};

Object Constructor

You can also use the Object() constructor:

let person = new Object();


person.firstName = "John";
person.lastName = "Doe";
person.age = 30;
Object.create() Method

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}`;
}
};

let person = Object.create(personProto);


person.firstName = "John";
person.lastName = "Doe";

console.log(person.greet()); // "Hello, my name is John Doe"

5.2.2 Accessing Object Properties

There are two main ways to access object properties:

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"

// Using variables as property names


let propertyName = "age";
console.log(person[propertyName]); // 30

When to Use Each Notation

• 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

5.2.3 Object Methods

Objects can contain functions as properties, which are called methods:

let person = {
firstName: "John",
lastName: "Doe",

// Method
fullName: function() {
return this.firstName + " " + this.lastName;
},

// ES6 shorthand method syntax


greet() {
return `Hello, my name is ${this.firstName}`;
}
};

console.log(person.fullName()); // "John Doe"


console.log(person.greet()); // "Hello, my name is John"

The this Keyword

In object methods, this refers to the object the method belongs to:

let person = {
name: "John",
sayName() {
console.log(this.name);
}
};
person.sayName(); // "John"

Be careful with this in nested functions or when passing methods as callbacks:

let person = {
name: "John",

// Problem with this in nested function


delayedGreet() {
setTimeout(function() {
console.log(`Hello, ${this.name}`); // this is not
person here
}, 1000);
},

// Solution 1: Use arrow function


delayedGreetArrow() {
setTimeout(() => {
console.log(`Hello, ${this.name}`); // this is
person here
}, 1000);
},

// Solution 2: Store this in a variable


delayedGreetVar() {
let self = this;
setTimeout(function() {
console.log(`Hello, ${self.name}`);
}, 1000);
},

// Solution 3: Use bind


delayedGreetBind() {
setTimeout(function() {
console.log(`Hello, ${this.name}`);
}.bind(this), 1000);
}
};

5.2.4 Object Iteration

There are several ways to iterate over object properties:

for...in Loop

The for...in loop iterates over all enumerable properties of an object:


let person = {
firstName: "John",
lastName: "Doe",
age: 30
};

for (let key in person) {


console.log(`${key}: ${person[key]}`);
}
// Output:
// firstName: John
// lastName: Doe
// age: 30

Object.keys(), Object.values(), and Object.entries() (ES2017)

These methods return arrays that you can iterate over:

let person = {
firstName: "John",
lastName: "Doe",
age: 30
};

// Object.keys() returns an array of property names


let keys = Object.keys(person);
console.log(keys); // ["firstName", "lastName", "age"]

// Object.values() returns an array of property values


let values = Object.values(person);
console.log(values); // ["John", "Doe", 30]

// Object.entries() returns an array of [key, value] pairs


let entries = Object.entries(person);
console.log(entries);
// [["firstName", "John"], ["lastName", "Doe"], ["age", 30]]

// You can use these with array methods


Object.keys(person).forEach(key => {
console.log(`${key}: ${person[key]}`);
});

// Or with for...of
for (let [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}
Object Manipulation

Adding and Modifying Properties

let person = {
firstName: "John",
lastName: "Doe"
};

// Adding new properties


person.age = 30;
person["email"] = "[email protected]";

// Modifying existing properties


person.firstName = "Jane";
person["lastName"] = "Smith";

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"];

console.log(person); // { firstName: "John", lastName: "Doe" }

Checking if a Property Exists

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)

// Using undefined check (be careful with properties that are


explicitly set to undefined)
console.log(person.firstName !== undefined); // true
console.log(person.email !== undefined); // false

Object Methods and Properties

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 }

Object.freeze() and Object.seal()

These methods control how mutable an object is:

let person = {
firstName: "John",
lastName: "Doe",
age: 30
};

// Object.freeze() prevents adding, removing, or changing


properties
Object.freeze(person);
person.age = 31; // Silently fails in non-strict mode
person.email = "[email protected]"; // Silently fails
delete person.age; // Silently fails
console.log(person); // Still { firstName: "John", lastName:
"Doe", age: 30 }
console.log(Object.isFrozen(person)); // true

// Object.seal() prevents adding or removing properties, but


allows changing existing ones
let user = {
username: "johndoe",
password: "secret"
};

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

Object Destructuring (ES6)

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"

// Assigning to different variable names


let { firstName: first, lastName: last } = person;
console.log(first, last); // "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" } }

Computed Property Names (ES6)

You can use expressions for property names in object literals:

let propName = "firstName";

let person = {
[propName]: "John", // Computed property name
["last" + "Name"]: "Doe",
[`age_${new Date().getFullYear() - 1990}`]: 30
};

console.log(person); // { firstName: "John", lastName: "Doe",


age_30: 30 }

Property Shorthand (ES6)

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
};

// You can write


let person2 = {
firstName,
lastName,
age
};

console.log(person2); // { firstName: "John", lastName: "Doe",


age: 30 }

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

JavaScript provides two methods for working with JSON:

JSON.stringify()

Converts a JavaScript value to a JSON string:

let person = {
firstName: "John",
lastName: "Doe",
age: 30,
isEmployed: true,
skills: ["JavaScript", "HTML", "CSS"],
greet: function() { return "Hello"; } // This will be
ignored in JSON
};

let jsonString = JSON.stringify(person);


console.log(jsonString);
// {"firstName":"John","lastName":"Doe","age":
30,"isEmployed":true,"skills":["JavaScript","HTML","CSS"]}

// Pretty-print with indentation


let prettyJson = JSON.stringify(person, null, 2);
console.log(prettyJson);
// {
// "firstName": "John",
// "lastName": "Doe",
// "age": 30,
// "isEmployed": true,
// "skills": [
// "JavaScript",
// "HTML",
// "CSS"
// ]
// }

// Custom replacer function


let jsonWithReplacer = JSON.stringify(person, (key, value) => {
if (key === "age") return value + 1;
return value;
});
console.log(JSON.parse(jsonWithReplacer).age); // 31

JSON.parse()

Parses a JSON string and returns a JavaScript value:


let jsonString = '{"firstName":"John","lastName":"Doe","age":
30}';
let person = JSON.parse(jsonString);

console.log(person.firstName); // "John"
console.log(person.age); // 30

// Custom reviver function


let jsonWithDate =
'{"name":"John","birthDate":"1990-01-01T00:00:00.000Z"}';
let personWithDate = JSON.parse(jsonWithDate, (key, value) => {
if (key === "birthDate") return new Date(value);
return value;
});

console.log(personWithDate.birthDate instanceof Date); // true


console.log(personWithDate.birthDate.getFullYear()); // 1990

Common Use Cases for JSON

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

5.4 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• Arrays: Creating, accessing, and manipulating ordered collections of values


• Array methods for adding, removing, searching, and transforming elements
• Array iteration techniques

• Array destructuring

• Objects: Working with collections of key-value pairs

• Creating objects and accessing their properties


• Object methods and the this keyword
• Iterating over object properties
• Object manipulation techniques
• Object destructuring and computed property names

• JSON: Converting between JavaScript objects and JSON strings

• JSON.stringify() for serialization


• JSON.parse() for deserialization
• Common use cases for JSON

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

1. Create an array of your favorite books (at least 5 books).


2. Use array methods to:
3. Add a new book to the beginning of the array
4. Add a new book to the end of the array
5. Remove the first book
6. Find the index of a specific book

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).

Exercise 3: Array and Object Manipulation

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

6. Create a new array with just the names of people over 30

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 a complex JavaScript object with nested objects and arrays.


2. Convert it to a JSON string and back to a JavaScript object.
3. Write a function that reads a JSON file and displays its contents in a formatted way.
4. Create a function that takes an object and removes any properties that cannot be
serialized to JSON.

Exercise 5: Practical Application

Create a simple inventory management system:

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.

Chapter 6: Document Object Model (DOM)


The Document Object Model (DOM) is a programming interface for web documents. It
represents the structure of HTML and XML documents as a tree of objects that can be
manipulated with JavaScript. Understanding the DOM is essential for creating dynamic,
interactive web applications.
6.1 Understanding the DOM
The DOM represents an HTML document as a hierarchical tree structure where each part
of the document is a node. This tree-like representation allows JavaScript to access and
manipulate the document's content, structure, and style.

The DOM Tree Structure

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

The DOM consists of different types of nodes:

1. Document Node: The root node representing the entire document


2. Element Nodes: Represent HTML elements (e.g., <div> , <p> , <h1> )
3. Text Nodes: Contain the text within elements
4. Attribute Nodes: Represent attributes of elements
5. Comment Nodes: Represent HTML comments

DOM Interfaces

The DOM provides various interfaces (object types) that you can use to work with the
document:

• Document: Represents the entire HTML document


• Element: Represents an element in the document
• NodeList: Represents a collection of nodes
• Attr: Represents an attribute of an element
• Text: Represents text content within an element
Examining the DOM

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...

The window Object

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:

• The DOM through window.document (or simply document )


• Browser features like window.location , window.history , etc.
• Timing functions like setTimeout and setInterval
• And much more

// These are equivalent


document.getElementById('main-heading');
window.document.getElementById('main-heading');
6.2 Selecting DOM Elements
To manipulate elements on a page, you first need to select them. JavaScript provides
several methods to select DOM elements.

6.2.1 getElementById

The getElementById method selects an element based on its id attribute:

// HTML: <h1 id="main-heading">Welcome to the DOM</h1>


const heading = document.getElementById('main-heading');
console.log(heading); // <h1 id="main-heading">Welcome to the
DOM</h1>

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

The getElementsByClassName method selects elements based on their class


attribute:

// HTML: <span class="highlight">paragraph</span>


const highlights = document.getElementsByClassName('highlight');
console.log(highlights); // HTMLCollection [span.highlight]
console.log(highlights.length); // 1
console.log(highlights[0]); // <span
class="highlight">paragraph</span>

This method returns an HTMLCollection, which is an array-like object containing all


matching elements. Even if there's only one matching element, it still returns a
collection.

6.2.3 getElementsByTagName

The getElementsByTagName method selects elements based on their tag name:

// HTML: <li>Item 1</li><li>Item 2</li><li>Item 3</li>


const listItems = document.getElementsByTagName('li');
console.log(listItems); // HTMLCollection(3) [li, li, li]
console.log(listItems.length); // 3

// Iterate through the collection


for (let i = 0; i < listItems.length; i++) {
console.log(listItems[i].textContent);
}
// Output:
// Item 1
// Item 2
// Item 3

Like getElementsByClassName , this method returns an HTMLCollection.

6.2.4 querySelector and querySelectorAll

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:

// Select the first paragraph element


const paragraph = document.querySelector('p');

// Select an element with a specific ID


const heading = document.querySelector('#main-heading');

// Select the first element with a specific class


const highlight = document.querySelector('.highlight');

// Select the first list item inside a container div


const firstItem = document.querySelector('.container li');

// More complex selectors


const spanInParagraph = document.querySelector('p > span');

querySelectorAll

The querySelectorAll method returns all elements that match the specified CSS
selector:

// Select all list items


const allItems = document.querySelectorAll('li');
console.log(allItems); // NodeList(3) [li, li, li]

// Select all elements with a specific class


const allHighlights = document.querySelectorAll('.highlight');
// Select all paragraphs and headings
const textElements = document.querySelectorAll('p, h1, h2, h3');

// Select all list items in a specific container


const containerItems = document.querySelectorAll('.container
li');

querySelectorAll returns a NodeList, which is similar to an HTMLCollection but has


some additional methods like forEach :

const listItems = document.querySelectorAll('li');

// Using forEach with NodeList


listItems.forEach(item => {
console.log(item.textContent);
});

HTMLCollection vs. NodeList

It's important to understand the differences between these collection types:

1. HTMLCollection:
2. Live collection (automatically updates when the DOM changes)
3. No built-in forEach method

4. Returned by methods like getElementsByClassName and


getElementsByTagName

5. NodeList:

6. Can be live or static (depending on how it's created)


7. Has forEach method
8. Returned by querySelectorAll (static) and childNodes (live)

Converting collections to arrays:

// Convert HTMLCollection to Array


const listItemsArray1 =
Array.from(document.getElementsByTagName('li'));
const listItemsArray2 =
[...document.getElementsByTagName('li')];

// Now you can use array methods


listItemsArray1.map(item => item.textContent);
listItemsArray2.filter(item =>
item.textContent.includes('Item'));

Selecting Elements Within Elements

You can also select elements within other elements by calling these methods on an
element instead of the document:

// Select the container first


const container = document.querySelector('.container');

// Then select elements within the container


const listInContainer = container.querySelector('ul');
const itemsInContainer = container.querySelectorAll('li');

This is useful for limiting the scope of your selection and improving performance when
working with large documents.

6.3 Manipulating DOM Elements


Once you've selected elements, you can manipulate them in various ways.

6.3.1 Changing Content

There are several ways to change the content of an element:

textContent

The textContent property gets or sets the text content of an element and all its
descendants:

const heading = document.getElementById('main-heading');

// Get the text content


console.log(heading.textContent); // "Welcome to the DOM"

// Set the text content


heading.textContent = "DOM Manipulation is Fun!";

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');

// Get the HTML content


console.log(paragraph.innerHTML); // "This is a <span
class="highlight">paragraph</span> about the DOM."

// Set the HTML content


paragraph.innerHTML = "This is <strong>bold</strong> and
<em>emphasized</em> text.";

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

The innerText property is similar to textContent but with some differences:

const paragraph = document.querySelector('p');

// Get the visible text content


console.log(paragraph.innerText);

// Set the text content


paragraph.innerText = "New text content";

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

6.3.2 Changing Attributes

You can get, set, check, and remove attributes using various methods and properties:

Standard Attributes

const link = document.querySelector('a');

// 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');

// Check if attribute exists


console.log(link.hasAttribute('target')); // false

// Remove attribute
link.removeAttribute('target');

Data Attributes

HTML5 introduced data attributes, which allow you to store custom data on elements:

<div id="user" data-id="123" data-user-name="John" data-


role="admin">John Doe</div>

You can access these with the dataset property:

const user = document.getElementById('user');

// Get data attributes


console.log(user.dataset.id); // "123"
console.log(user.dataset.userName); // "John" (note camelCase
conversion)
console.log(user.dataset.role); // "admin"

// Set data attributes


user.dataset.status = "active";
user.dataset.lastLogin = "2023-05-15";

Class Manipulation

Working with classes is so common that there's a dedicated classList property:

const element = document.querySelector('.container');

// Add classes
element.classList.add('highlight', 'active');

// Remove classes
element.classList.remove('active');

// Toggle a class (add if not present, remove if present)


element.classList.toggle('visible'); // returns true if added,
false if removed

// Check if an element has a class


if (element.classList.contains('highlight')) {
console.log('Element has highlight class');
}

// Replace one class with another


element.classList.replace('highlight', 'selected');

You can also set the entire className property, but this replaces all existing classes:

element.className = 'new-class another-class';

6.3.3 Changing Styles

You can modify an element's style directly using the style property:

const heading = document.getElementById('main-heading');

// Set individual style properties


heading.style.color = 'blue';
heading.style.backgroundColor = 'yellow';
heading.style.padding = '10px';
heading.style.borderRadius = '5px';

// Note: CSS properties with hyphens are converted to camelCase


// background-color -> backgroundColor
// border-radius -> borderRadius

Getting computed styles:

const element = document.querySelector('.container');


const computedStyle = window.getComputedStyle(element);

console.log(computedStyle.width); // "500px"
console.log(computedStyle.display); // "block"
console.log(computedStyle.getPropertyValue('margin-top')); //
"20px"

6.3.4 Creating and Removing Elements

The DOM API allows you to create new elements and add them to the document.

Creating Elements

// Create a new element


const newParagraph = document.createElement('p');
// Add content to the element
newParagraph.textContent = 'This is a dynamically created
paragraph.';

// Add attributes
newParagraph.id = 'dynamic-paragraph';
newParagraph.classList.add('highlight');

// Create a text node


const textNode = document.createTextNode('This is a text
node.');

// Create a comment
const comment = document.createComment('This is a comment');

Adding Elements to the DOM

There are several ways to add elements to the DOM:

const container = document.querySelector('.container');


const newParagraph = document.createElement('p');
newParagraph.textContent = 'New paragraph';

// Append at the end (newer method)


container.append(newParagraph);

// Append at the end (older method)


container.appendChild(newParagraph);

// Insert at the beginning


container.prepend(newParagraph);

// Insert before a specific element


const referenceElement = container.querySelector('ul');
container.insertBefore(newParagraph, referenceElement);

// Insert adjacent to an element


referenceElement.insertAdjacentElement('beforebegin',
newParagraph); // Before the element
referenceElement.insertAdjacentElement('afterbegin',
newParagraph); // Inside the element, before its first child
referenceElement.insertAdjacentElement('beforeend',
newParagraph); // Inside the element, after its last child
referenceElement.insertAdjacentElement('afterend',
newParagraph); // After the element

// Insert adjacent HTML


referenceElement.insertAdjacentHTML('beforeend', '<p>HTML
content</p>');
// Insert adjacent text
referenceElement.insertAdjacentText('afterend', 'Plain text');

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

// append multiple nodes


container.append(paragraph1, paragraph2, 'Text node');

// appendChild only takes one node


container.appendChild(paragraph1);
container.appendChild(paragraph2);
// container.appendChild('Text node'); // Error: Not a valid
node

Cloning Elements

You can create copies of existing elements:

const original = document.querySelector('ul');

// Clone without children


const shallowClone = original.cloneNode(false);

// Clone with all descendants


const deepClone = original.cloneNode(true);

// Add the clone to the document


document.body.appendChild(deepClone);

Removing Elements

There are several ways to remove elements from the DOM:

const elementToRemove = document.querySelector('.to-remove');

// Modern method
elementToRemove.remove();

// Traditional method (remove child from parent)


if (elementToRemove.parentNode) {
elementToRemove.parentNode.removeChild(elementToRemove);
}
// Replace an element
const parent = document.querySelector('.container');
const oldElement = document.querySelector('.old');
const newElement = document.createElement('div');
newElement.textContent = 'New element';

parent.replaceChild(newElement, oldElement);

// Replace all content


parent.innerHTML = ''; // Remove all children
parent.textContent = ''; // Remove all children and text

Practical Example: Creating a Dynamic List

Let's put these concepts together to create a dynamic list:

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';

// Add list items


items.forEach((item, index) => {
const listItem = document.createElement('li');
listItem.textContent = item;
listItem.dataset.index = index;
listItem.classList.add('list-item');

// Add delete button


const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.className = 'delete-btn';
deleteButton.addEventListener('click', function() {
listItem.remove();
});

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']);

6.4 Traversing the DOM


DOM traversal refers to navigating through the DOM tree to find specific elements
relative to other elements.

Parent Relationships

const listItem = document.querySelector('li');

// Get parent node


const list = listItem.parentNode;
// or
const list2 = listItem.parentElement;

// Get the closest ancestor matching a selector


const container = listItem.closest('.container');

The difference between parentNode and parentElement is that parentNode can


be any node type, while parentElement is always an element node or null.

Child Relationships

const list = document.querySelector('ul');

// Get all child nodes (including text nodes, comments, etc.)


const childNodes = list.childNodes;

// Get all child elements (only element nodes)


const children = list.children;

// Get first and last child (any node type)


const firstChild = list.firstChild;
const lastChild = list.lastChild;

// Get first and last element child (only element nodes)


const firstElement = list.firstElementChild;
const lastElement = list.lastElementChild;
// Check if an element has child nodes
if (list.hasChildNodes()) {
console.log('The list has child nodes');
}

Sibling Relationships

const listItem = document.querySelector('li');

// Get next sibling (any node type)


const nextSibling = listItem.nextSibling;

// Get previous sibling (any node type)


const prevSibling = listItem.previousSibling;

// Get next element sibling (only element nodes)


const nextElement = listItem.nextElementSibling;

// Get previous element sibling (only element nodes)


const prevElement = listItem.previousElementSibling;

Practical Example: Creating a DOM Tree Visualizer

Let's create a simple function that visualizes the DOM structure of an element:

function visualizeDOMTree(element, depth = 0) {


// Create indentation based on depth
const indent = ' '.repeat(depth);

// Get element tag name


const tagName = element.tagName.toLowerCase();

// Get element ID and classes


const id = element.id ? `#${element.id}` : '';
const classes = element.className ? `.$
{element.className.split(' ').join('.')}` : '';

// Log the element


console.log(`${indent}${tagName}${id}${classes}`);

// Recursively process child elements


Array.from(element.children).forEach(child => {
visualizeDOMTree(child, depth + 1);
});
}

// Usage
const container = document.querySelector('.container');
visualizeDOMTree(container);

// Output might look like:


// div.container
// h2
// ul.list
// li.list-item
// li.list-item
// li.list-item

6.5 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• 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

Exercise 1: DOM Selection

Create an HTML page with various elements and practice selecting them:

1. Select an element by ID and change its text content


2. Select all elements with a specific class and change their background color
3. Select all paragraphs and add a border to them
4. Use querySelector to select the first list item in a specific list
5. Use querySelectorAll to select all links in the document and log their href
attributes
Exercise 2: DOM Manipulation

1. Create a function that dynamically generates a table from an array of objects


2. Create a function that changes the text color of all elements with a specific class
3. Create a function that adds a new class to every other list item in a list
4. Create a function that replaces all images with a placeholder image
5. Create a function that wraps every paragraph in a div with a specific class

Exercise 3: DOM Traversal

1. Create a function that highlights the parent of a clicked element


2. Create a function that toggles the visibility of all siblings of a clicked element
3. Create a function that counts and displays the number of children of a selected
element
4. Create a function that finds the closest heading element to a clicked paragraph
5. Create a function that selects all descendants of a specific type within an element

Exercise 4: Practical Application

Create a simple "Todo List" application that allows users to:

1. Add new todo items through an input field and button


2. Mark todo items as complete by clicking on them (apply a strikethrough style)
3. Delete todo items with a delete button
4. Filter todos to show all, only active, or only completed items
5. Display a count of remaining active todo items

Exercise 5: Advanced DOM Manipulation

1. Create a function that builds a responsive navigation menu from a configuration


object
2. Create a function that generates a form with validation based on a schema object
3. Create a function that converts a flat array of objects into a nested tree structure in
the DOM
4. Create a function that implements a simple drag-and-drop interface for reordering
list items
5. Create a function that lazy-loads images as they scroll into view

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.

7.1 Event Types


JavaScript supports a wide variety of events. Here are some of the most common
categories:

Mouse Events

• click : Occurs when an element is clicked


• dblclick : Occurs when an element is double-clicked
• mousedown : Occurs when a mouse button is pressed down over an element
• mouseup : Occurs when a mouse button is released over an element
• mousemove : Occurs when the mouse pointer moves over an element
• mouseover : Occurs when the mouse pointer enters an element
• mouseout : Occurs when the mouse pointer leaves an element
• mouseenter : Similar to mouseover, but doesn't bubble and only triggered when
pointer enters the target element
• mouseleave : Similar to mouseout, but doesn't bubble and only triggered when
pointer leaves the target element

Keyboard Events

• keydown : Occurs when a key is pressed down


• keyup : Occurs when a key is released
• keypress : Occurs when a key that produces a character is pressed (deprecated in
favor of keydown )

Form Events

• submit : Occurs when a form is submitted


• reset : Occurs when a form is reset
• change : Occurs when the value of an input element changes (for <input> ,
<select> , and <textarea> )
• input : Occurs immediately when the value of an input element changes
• focus : Occurs when an element receives focus
• blur : Occurs when an element loses focus
• select : Occurs when text is selected in an input field or textarea

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

• touchstart : Occurs when a touch point is placed on the touch surface


• touchmove : Occurs when a touch point is moved along the touch surface
• touchend : Occurs when a touch point is removed from the touch surface
• touchcancel : Occurs when a touch point has been disrupted

Drag Events

• dragstart : Occurs when the user starts dragging an element


• drag : Occurs when an element is being dragged
• dragenter : Occurs when the dragged element enters a drop target
• dragleave : Occurs when the dragged element leaves a drop target
• dragover : Occurs when the dragged element is over a drop target
• drop : Occurs when the dragged element is dropped on a drop target
• dragend : Occurs when the drag operation ends

Media Events

• play : Occurs when media starts or resumes playing


• pause : Occurs when media is paused
• ended : Occurs when media has reached the end
• volumechange : Occurs when the volume is changed
• timeupdate : Occurs when the playing position has changed
Animation Events

• animationstart : Occurs when a CSS animation starts


• animationend : Occurs when a CSS animation completes
• animationiteration : Occurs when a CSS animation is repeated

Transition Events

• transitionstart : Occurs when a CSS transition starts


• transitionend : Occurs when a CSS transition completes
• transitionrun : Occurs when a CSS transition is created
• transitioncancel : Occurs when a CSS transition is canceled

7.2 Event Handlers


Event handlers are functions that are executed when a specific event occurs. There are
several ways to assign event handlers to elements.

HTML Attribute Event Handlers

You can assign event handlers directly in HTML using attributes:

<button onclick="alert('Button clicked!')">Click Me</button>

You can also call functions defined elsewhere:

<button onclick="handleClick()">Click Me</button>

<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

DOM Property Event Handlers

You can assign event handlers using DOM properties:


const button = document.querySelector('button');

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

// This will overwrite the previous handler


button.onclick = function() {
console.log('New handler');
};

Event Listeners

The most flexible way to handle events is using the addEventListener method:

const button = document.querySelector('button');

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

// Adding multiple listeners


button.addEventListener('click', function() {
console.log('First handler');
});

button.addEventListener('click', function() {
console.log('Second handler');
});

// Both handlers will execute when the button is clicked

Removing Event Listeners

To remove an event listener, you need a reference to the function:


function handleClick() {
console.log('Button clicked');
}

// Add the listener


button.addEventListener('click', handleClick);

// Later, remove the listener


button.removeEventListener('click', handleClick);

Note that you must provide the same function reference to remove the listener.
Anonymous functions cannot be removed directly:

// This won't work


button.addEventListener('click', function() {
console.log('Click');
});

// This anonymous function can't be referenced later for removal

To solve this, store the function in a variable:

const handleClick = function() {


console.log('Click');
};

button.addEventListener('click', handleClick);

// Later
button.removeEventListener('click', handleClick);

Event Listener Options

The addEventListener method accepts an optional third parameter, which can be a


boolean or an options object:

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:

// This listener will automatically be removed after the first


click
button.addEventListener('click', function() {
console.log('This will only run once');
}, { once: true });

7.3 The Event Object


When an event occurs, the browser creates an event object with details about the event
and passes it to the event handler. This object contains properties and methods that
provide information about the event and allow you to control its behavior.

Accessing the Event Object

The event object is automatically passed to event handlers:

// Using DOM property


button.onclick = function(event) {
console.log(event.type); // "click"
};

// Using addEventListener
button.addEventListener('click', function(event) {
console.log(event.type); // "click"
});

// In HTML attribute (not recommended)


<button onclick="handleClick(event)">Click</button>
<script>
function handleClick(event) {
console.log(event.type); // "click"
}
</script>

Common Event Object Properties

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

Different event types have their own specific properties:

Mouse Event 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)
});

Keyboard Event Properties

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 Event Properties

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();

// Stop the event from bubbling up


event.stopPropagation();

// Stop the event from bubbling and prevent any other


listeners on this element
event.stopImmediatePropagation();
});

preventDefault()

The preventDefault() method stops the browser from executing the default action
of an event. Common uses include:

• Preventing form submission to handle it with JavaScript


• Preventing a link from navigating to a new page
• Preventing text selection or context menu

// Prevent form submission


form.addEventListener('submit', function(event) {
event.preventDefault();
// Handle form submission with JavaScript
});

// Prevent link navigation


link.addEventListener('click', function(event) {
event.preventDefault();
// Do something else instead
});

stopPropagation()

The stopPropagation() method stops the event from bubbling up to parent


elements:

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()

The stopImmediatePropagation() method stops the event from bubbling and


prevents other listeners on the same element from being called:

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>

When the button is clicked, the output will be:

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');
});

When the button is clicked, the output will be:

Outer div - Capture phase


Inner div - Capture phase
Button - Capture phase
Button - Bubble phase
Inner div - Bubble phase
Outer div - Bubble phase
The Complete Event Flow

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)

7.5 Event Delegation


Event delegation is a technique where you attach a single event listener to a parent
element instead of multiple listeners on child elements. This works because of event
bubbling.

Benefits of Event Delegation

1. Memory efficiency: Fewer event listeners means less memory usage


2. Dynamic elements: Works with elements added to the DOM after the initial page
load
3. Less code: Cleaner, more maintainable code

Basic Event Delegation Example

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>

This works even if you add new buttons dynamically:

// Add a new button


const newItem = document.createElement('li');
newItem.innerHTML = '<button data-action="export">Export</
button>';
document.getElementById('menu').appendChild(newItem);

// The existing event handler will work for this new button

Using closest() for Better Delegation

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>

Handling Different Event Types

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');
});

7.6 Custom Events


In addition to the built-in events, you can create and dispatch your own custom events.

Creating Custom Events

You can create custom events using the CustomEvent constructor:

// Create a custom event


const customEvent = new CustomEvent('userLoggedIn', {
detail: {
username: 'john_doe',
timestamp: new Date()
},
bubbles: true,
cancelable: true
});
// Dispatch the event
document.dispatchEvent(customEvent);

The detail property can contain any data you want to pass with the event.

Listening for Custom Events

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}`);
});

Creating and Dispatching Events

You can also use the older Event constructor for simpler events:

// Create a simple event


const simpleEvent = new Event('simpleCustomEvent', {
bubbles: true,
cancelable: true
});

// Dispatch on a specific element


document.getElementById('myElement').dispatchEvent(simpleEvent);

Practical Example: Component Communication

Custom events are useful for communication between components:

// Shopping cart component


class ShoppingCart {
addItem(item) {
// Add item to cart
console.log(`Added ${item.name} to cart`);

// Dispatch custom event


const event = new CustomEvent('cartUpdated', {
bubbles: true,
detail: {
itemCount: this.getItemCount(),
totalPrice: this.getTotalPrice(),
lastItemAdded: item
}
});

document.dispatchEvent(event);
}

getItemCount() {
return 5; // Simplified for example
}

getTotalPrice() {
return 99.95; // Simplified for example
}
}

// Cart summary component


class CartSummary {
constructor() {
// Listen for cart updates
document.addEventListener('cartUpdated',
this.updateSummary.bind(this));
}

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 });

7.7 Practical Event Handling Patterns


Let's explore some common patterns and best practices for event handling.
Debouncing and Throttling

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:

function debounce(func, delay) {


let timeoutId;

return function(...args) {
// Clear the previous timeout
clearTimeout(timeoutId);

// Set a new timeout


timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

// 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:

function throttle(func, limit) {


let inThrottle;

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.

Event Handling in Single Page Applications

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'));

// Later, when component is no longer needed


component.destroy();

Event Handling with Promises

You can wrap event handling in promises for more readable asynchronous code:

function waitForEvent(element, eventName) {


return new Promise(resolve => {
const handler = event => {
element.removeEventListener(eventName, handler);
resolve(event);
};
element.addEventListener(eventName, handler);
});
}

// Usage
async function handleFormSubmission() {
console.log('Waiting for form submission...');

const event = await waitForEvent(form, 'submit');


event.preventDefault();

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:

const button = document.getElementById('custom-button');

button.addEventListener('click', handleAction);

// Also handle keyboard events for accessibility


button.addEventListener('keydown', function(event) {
// 13 is Enter, 32 is Space
if (event.keyCode === 13 || event.keyCode === 32) {
event.preventDefault(); // Prevent scrolling on Space
handleAction(event);
}
});

function handleAction(event) {
console.log('Action triggered');
// Perform action
}

7.8 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• 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

Exercise 1: Basic Event Handling

1. Create a button that changes its text when clicked


2. Create a form that validates input as the user types
3. Create a div that changes color when the mouse hovers over it
4. Create a keyboard shortcut that triggers an action when specific keys are pressed
5. Create a custom dropdown menu that opens on click and closes when clicking
outside

Exercise 2: Event Propagation

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

Exercise 3: Event Delegation

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

Exercise 4: Custom Events

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

Exercise 5: Practical Application

Create a simple image gallery application that:

1. Displays a grid of thumbnail images


2. Shows a larger version of the image when a thumbnail is clicked
3. Allows navigation between images using next/previous buttons
4. Implements keyboard navigation (arrow keys)
5. Closes the large image when clicking outside of it or pressing the Escape key
6. Uses event delegation for handling thumbnail clicks
7. Implements a simple lightbox effect with CSS transitions
8. Uses custom events to notify when images are loaded or when the gallery state
changes

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.

Chapter 8: Advanced Objects and


Prototypes
JavaScript is a prototype-based language, which means objects can inherit properties
and methods directly from other objects. Understanding JavaScript's object-oriented
nature and prototype system is crucial for writing efficient, maintainable code. In this
chapter, we'll explore advanced object concepts, prototypes, and inheritance in
JavaScript.

8.1 Object-Oriented Programming in JavaScript


Object-Oriented Programming (OOP) is a programming paradigm based on the concept
of "objects" that contain data (properties) and code (methods). JavaScript supports
OOP, but it implements it differently from class-based languages like Java or C++.

Core OOP Concepts in JavaScript

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()}`;
}
};

console.log(person.greet()); // "Hello, my name is John Doe"

2. Abstraction

Abstraction means hiding complex implementation details and showing only the
necessary features:

// Database abstraction example


const database = {
// Private data (by convention, not truly private)
_data: [],
_connection: null,

// 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:

// Basic inheritance using Object.create()


const animal = {
type: "Unknown",
makeSound() {
console.log("Some generic sound");
}
};

const dog = Object.create(animal);


dog.type = "Dog";
dog.makeSound = function() {
console.log("Woof!");
};

const cat = Object.create(animal);


cat.type = "Cat";
cat.makeSound = function() {
console.log("Meow!");
};

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!"

Different Ways to Create Objects

JavaScript provides several ways to create objects:


1. Object Literals

The simplest way to create an object:

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:

function Person(name, age) {


this.name = name;
this.age = age;
this.greet = function() {
return `Hello, my name is ${this.name}`;
};
}

const john = new Person("John", 30);


const jane = new Person("Jane", 25);

console.log(john.greet()); // "Hello, my name is John"


console.log(jane.greet()); // "Hello, my name is Jane"

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}`;
}
};

const john = Object.create(personProto);


john.name = "John";
john.age = 30;

console.log(john.greet()); // "Hello, my name is John"


4. ES6 Classes

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}`;
}
}

const john = new Person("John", 30);


console.log(john.greet()); // "Hello, my name is John"

8.2 Constructor Functions


Constructor functions are a traditional way to create objects in JavaScript. They serve as
templates for creating multiple objects with the same properties and methods.

Basic Constructor Function

function Car(make, model, year) {


this.make = make;
this.model = model;
this.year = year;
this.isRunning = false;

this.start = function() {
this.isRunning = true;
return `${this.make} ${this.model} started`;
};

this.stop = function() {
this.isRunning = false;
return `${this.make} ${this.model} stopped`;
};
}

const civic = new Car("Honda", "Civic", 2020);


const accord = new Car("Honda", "Accord", 2021);
console.log(civic.start()); // "Honda Civic started"
console.log(accord.start()); // "Honda Accord started"

The new Keyword

When you use the new keyword with a constructor function, several things happen:

1. A new empty object is created


2. The constructor function is called with this set to the new object
3. The new object is linked to the constructor's prototype
4. The function implicitly returns the object (unless it explicitly returns another
object)

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

// Using new
const john = new Person("John");
console.log(john.name); // "John"

// What happens if you forget new?


const jane =
Person("Jane"); // this points to global object (or undefined in
strict mode)
console.log(jane); // undefined
console.log(window.name); // "Jane" (in browser, not in strict
mode)

Constructor Function Best Practices

1. Capitalize Constructor Names

By convention, constructor functions start with a capital letter:

function Car() { } // Good


function car() { } // Bad - looks like a regular function

2. Use Strict Mode

Using strict mode prevents accidental global variables if new is forgotten:

function Person(name) {
"use strict";
this.name = name;
}

const john = Person("John"); // TypeError: Cannot set property


'name' of undefined

3. Enforce new with Safeguards

You can add safeguards to ensure new is used:

function Person(name) {
if (!(this instanceof Person)) {
return new Person(name);
}
this.name = name;
}

const john = Person("John"); // Still works correctly


console.log(john.name); // "John"

4. Move Methods to the Prototype

To save memory, move methods to the prototype instead of creating them in each
instance:

function Car(make, model) {


this.make = make;
this.model = model;
this.isRunning = false;
}

// Methods on prototype (shared across all instances)


Car.prototype.start = function() {
this.isRunning = true;
return `${this.make} ${this.model} started`;
};

Car.prototype.stop = function() {
this.isRunning = false;
return `${this.make} ${this.model} stopped`;
};

const civic = new Car("Honda", "Civic");


const accord = new Car("Honda", "Accord");

console.log(civic.start()); // "Honda Civic started"


console.log(accord.start()); // "Honda Accord started"
// Both instances share the same method
console.log(civic.start === accord.start); // true

8.3 Prototypes and Inheritance


Prototypes are a fundamental concept in JavaScript. Every JavaScript object has a
prototype, which is another object that it inherits properties and methods from.

The Prototype Chain

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".

// Simple prototype chain example


const animal = {
makeSound() {
return "Some generic sound";
}
};

const dog = Object.create(animal);


dog.makeSound = function() {
return "Woof!";
};

const puppy = Object.create(dog);

console.log(animal.makeSound()); // "Some generic sound"


console.log(dog.makeSound()); // "Woof!"
console.log(puppy.makeSound()); // "Woof!" (inherited from dog)

Accessing an Object's Prototype

There are several ways to access an object's prototype:

// Using Object.getPrototypeOf()
const dogProto = Object.getPrototypeOf(dog);
console.log(dogProto === animal); // true

// Using __proto__ (deprecated but widely supported)


console.log(dog.__proto__ === animal); // true

// For constructor functions


console.log(Dog.prototype); // The prototype object that will be
assigned to instances

Setting an Object's Prototype

You can set an object's prototype in several ways:

// Using Object.create()
const animal = { eats: true };
const dog = Object.create(animal);
console.log(dog.eats); // true

// Using Object.setPrototypeOf() (not recommended for


performance reasons)
const cat = {};
Object.setPrototypeOf(cat, animal);
console.log(cat.eats); // true

// Using constructor functions


function Bird() { }
Bird.prototype = { flies: true };
const sparrow = new Bird();
console.log(sparrow.flies); // true

Prototype-Based Inheritance

Let's implement inheritance using prototypes:

// 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!`;
};

// Override parent methods


Dog.prototype.eat = function() {
return `${this.name} is eating dog food`;
};

// Create instances
const generic = new Animal("Generic Animal");
const rex = new Dog("Rex", "German Shepherd");

console.log(generic.eat()); // "Generic Animal is eating"


console.log(rex.eat()); // "Rex is eating dog food"
console.log(rex.bark()); // "Rex says woof!"

The instanceof Operator

The instanceof operator tests whether an object has a constructor's prototype in its
prototype chain:

console.log(rex instanceof Dog); // true


console.log(rex instanceof Animal); // true
console.log(generic instanceof Dog); // false

Checking Property Ownership

To check if a property belongs to an object itself or is inherited:

const dog = { breed: "Labrador" };


const puppy = Object.create(dog);
puppy.age = 1;

// Check if property exists (own or inherited)


console.log("breed" in puppy); // true
console.log("age" in puppy); // true

// Check if property is own (not inherited)


console.log(puppy.hasOwnProperty("breed")); // false
console.log(puppy.hasOwnProperty("age")); // true
8.4 ES6 Classes
ES6 introduced class syntax to JavaScript, providing a more familiar way to define
objects and inheritance for developers coming from class-based languages. Under the
hood, ES6 classes still use prototypal inheritance.

8.4.1 Class Declaration

A basic class declaration:

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()}`;
}
}

const john = new Person("John", "Doe");


console.log(john.greet()); // "Hello, my name is John Doe"

8.4.2 Class Expression

Classes can also be defined using expressions:

// Unnamed class expression


const Person = class {
constructor(name) {
this.name = name;
}

greet() {
return `Hello, my name is ${this.name}`;
}
};

// Named class expression


const Employee = class EmployeeClass {
constructor(name, position) {
this.name = name;
this.position = position;
}

// EmployeeClass is only visible inside the class


describe() {
return `${this.name} works as ${this.position}`;
}
};

const john = new Person("John");


const jane = new Employee("Jane", "Developer");

console.log(john.greet()); // "Hello, my name is John"


console.log(jane.describe()); // "Jane works as Developer"

8.4.3 Class Inheritance

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 Dog extends Animal {


constructor(name, breed) {
super(name); // Call the parent constructor
this.breed = breed;
}

// Override the parent method


speak() {
return `${this.name} barks`;
}

// Add a new method


fetch() {
return `${this.name} fetches the ball`;
}
}

const animal = new Animal("Generic Animal");


const dog = new Dog("Rex", "German Shepherd");

console.log(animal.speak()); // "Generic Animal makes a noise"


console.log(dog.speak()); // "Rex barks"
console.log(dog.fetch()); // "Rex fetches the ball"

The super Keyword

The super keyword is used to call methods on the parent class:

class Animal {
constructor(name) {
this.name = name;
}

speak() {
return `${this.name} makes a noise`;
}
}

class Dog extends Animal {


speak() {
// Call the parent method first
const parentResult = super.speak();
return `${parentResult}, but specifically barks`;
}
}

const dog = new Dog("Rex");


console.log(dog.speak()); // "Rex makes a noise, but
specifically barks"

Class Hoisting

Unlike function declarations, class declarations are not hoisted:

// This works
const p1 = new FunctionPerson("John");
function FunctionPerson(name) {
this.name = name;
}

// This throws an error


const p2 = new ClassPerson("Jane"); // ReferenceError: Cannot
access 'ClassPerson' before initialization
class ClassPerson {
constructor(name) {
this.name = name;
}
}
Getters and Setters

Classes can define getter and setter methods:

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}`;
}
}

const john = new Person("John", "Doe");


console.log(john.fullName); // "John Doe"

john.fullName = "Jane Smith";


console.log(john._firstName); // "Jane"
console.log(john._lastName); // "Smith"
console.log(john.greet()); // "Hello, my name is Jane Smith"

Computed Property Names

Like object literals, classes can use computed property names:

const methodName = "sayHello";

class Greeter {
[methodName]() {
return "Hello!";
}

[`say${"Hi"}`]() {
return "Hi!";
}
}

const greeter = new Greeter();


console.log(greeter.sayHello()); // "Hello!"
console.log(greeter.sayHi()); // "Hi!"

8.5 Static Methods and Properties


Static methods and properties belong to the class itself, not to instances of the class.

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;
}
}

// Call static methods directly on the class


console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.subtract(10, 4)); // 6
console.log(MathUtils.multiply(2, 3)); // 6

// Static methods are not available on instances


const utils = new MathUtils();
// console.log(utils.add(1, 2)); // TypeError: utils.add is not
a function

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"

Practical Use Cases for Static Methods

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}`;
}
}

// Using the factory method


const john = Person.createFromFullName("John Doe");
console.log(john.getFullName()); // "John Doe"

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;
}
}

// Both variables reference the same instance


const db1 = new Database();
const db2 = new Database();
const db3 = Database.getInstance();

db1.add("Item 1");
db2.add("Item 2");

console.log(db3.getAll()); // ["Item 1", "Item 2"]


console.log(db1 === db2); // true
console.log(db2 === db3); // true
8.6 Private Fields and Methods
ES2020 introduced private class fields and methods, allowing true encapsulation in
JavaScript classes.

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}`;
}
}

const account = new BankAccount("123456789", 1000);


console.log(account.balance); // 1000
console.log(account.accountInfo); // "Account 123456789: $1000"

account.deposit(500);
console.log(account.balance); // 1500

account.withdraw(200);
console.log(account.balance); // 1300

// Private fields are not accessible outside the class


// console.log(account.#balance); // SyntaxError: Private field
'#balance' must be declared in an enclosing class

Private Methods

Private methods are also declared with a hash ( # ) prefix:

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()}` };
}
}

const processor = new PaymentProcessor("sk_test_123456789");


const result = processor.processPayment(99.99, "USD");
console.log(result); // { success: true, id:
"payment_1621234567890" }

// Private methods are not accessible outside the class


// processor.#validateAmount(100); // SyntaxError: Private field
'#validateAmount' must be declared in an enclosing class

Private Static Fields and Methods

You can also have private static fields and methods:

class AuthService {
// Private static field
static #instance = null;

// Private instance field


#token = null;

constructor() {
if (AuthService.#instance) {
return AuthService.#instance;
}

AuthService.#instance = this;
}

// Public static method


static getInstance() {
if (!AuthService.#instance) {
AuthService.#instance = new AuthService();
}
return AuthService.#instance;
}

// Private static method


static #validateToken(token) {
return token && token.length > 10;
}

// Public instance method


login(username, password) {
// In a real app, this would call an API
this.#token = `token_${username}_${Date.now()}`;
return this.#token;
}

setToken(token) {
if (AuthService.#validateToken(token)) {
this.#token = token;
return true;
}
return false;
}

getToken() {
return this.#token;
}
}

const auth = AuthService.getInstance();


auth.login("user123", "password");
console.log(auth.getToken()); // "token_user123_1621234567890"

// Private static methods are not accessible outside the class


// AuthService.#validateToken("test"); // SyntaxError: Private
field '#validateToken' must be declared in an enclosing class

8.7 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• Object-Oriented Programming in JavaScript: The core OOP concepts and how


they apply in JavaScript
• Constructor Functions: Creating objects using constructor functions and the new
keyword
• Prototypes and Inheritance: Understanding JavaScript's prototype-based
inheritance system
• ES6 Classes: Using modern class syntax for cleaner object-oriented code
• Static Methods and Properties: Creating class-level functionality
• Private Fields and Methods: Implementing true encapsulation with private class
features

JavaScript's object-oriented features provide powerful tools for organizing and


structuring your code. While JavaScript's approach to OOP differs from traditional class-
based languages, understanding these concepts allows you to write more maintainable,
reusable code.

Exercises

Exercise 1: Constructor Functions and Prototypes

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.

Exercise 2: ES6 Classes

1. Convert your Vehicle and Motorcycle constructors from Exercise 1 to ES6


classes.
2. Add getters and setters for the model property that validate the model is a non-
empty string.
3. Add a static method compare(vehicle1, vehicle2) that returns the newer
vehicle.
4. Create a Truck class that extends Vehicle and adds properties for
cargoCapacity and towingCapacity .
5. Implement a method in Truck called canCarry(weight) that checks if the
weight is within the cargo capacity.

Exercise 3: Private Fields and Methods

1. Create a BankAccount class with private fields for #accountNumber and


#balance .
2. Implement methods for deposit(amount) , withdraw(amount) , and
getBalance() .
3. Add a private method #validateAmount(amount) that checks if the amount is
positive.
4. Create a SavingsAccount class that extends BankAccount and adds a private
field for #interestRate .
5. Implement a method addInterest() that increases the balance based on the
interest rate.

Exercise 4: Object Composition

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.

Exercise 5: Practical Application

Create a simple library management system with the following classes:

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.

Chapter 9: Closures and Scopes


Understanding scope and closures is essential for mastering JavaScript. These concepts
are fundamental to how variables are accessed and functions behave in your code. In
this chapter, we'll explore JavaScript's scoping rules and the powerful closure
mechanism that enables many advanced programming patterns.
9.1 Understanding Scope
Scope determines the accessibility of variables, functions, and objects in your code
during runtime. In other words, scope defines where variables and functions can be
accessed in your program.

9.1.1 Global Scope

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.

// Variables in global scope


const globalVariable = "I'm global";
var anotherGlobalVariable = "I'm also global";

function someFunction() {
console.log(globalVariable); // "I'm global"
}

someFunction();
console.log(globalVariable); // "I'm global"

The Global Object

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

// Note: let and const don't create properties on the global


object
let y = 20;
console.log(window.y); // undefined

Problems with Global Variables

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

9.1.2 Function Scope

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);
}

showMessage(); // "Hello, world!"


// console.log(message); // ReferenceError: message is not
defined

Nested Function Scope

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:

const value = "global";

function checkScope() {
const value = "local";
console.log(value); // "local"
}

checkScope();
console.log(value); // "global"

9.1.3 Block Scope

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";

console.log(blockScoped); // "I'm block-scoped"


console.log(alsoBlockScoped); // "I'm also block-scoped"
}

// console.log(blockScoped); // ReferenceError: blockScoped


is not defined
// console.log(alsoBlockScoped); // ReferenceError:
alsoBlockScoped is not defined
console.log(notBlockScoped); // "I'm function-scoped"

Block Scope in Loops

Block scope is particularly useful in loops:

// Using var (problematic)


for (var i = 0; i < 3; i++) {
// i is function-scoped
}
console.log(i); // 3 (i is still accessible)

// Using let (better)


for (let j = 0; j < 3; j++) {
// j is block-scoped
}
// console.log(j); // ReferenceError: j is not defined

This becomes especially important when creating functions inside loops:

// Using var
function createFunctionsVar() {
var functions = [];

for (var i = 0; i < 3; i++) {


functions.push(function() {
console.log(i);
});
}

return functions;
}

const functionsVar = createFunctionsVar();


functionsVar[0](); // 3
functionsVar[1](); // 3
functionsVar[2](); // 3

// Using let
function createFunctionsLet() {
const functions = [];

for (let i = 0; i < 3; i++) {


functions.push(function() {
console.log(i);
});
}

return functions;
}

const functionsLet = createFunctionsLet();


functionsLet[0](); // 0
functionsLet[1](); // 1
functionsLet[2](); // 2

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.

9.2 Lexical Scope


JavaScript uses lexical (or static) scoping, which means the scope of a variable is
determined by its location in the source code, not by the function call stack.

const globalVar = "global";

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.

const global = "global";

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.

9.3.1 Creating Closures

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;
};
}

const counter = createCounter();


console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

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 .

Multiple Closures from the Same Function

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

Closures with Parameters

Closures can also capture function parameters:

function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}

const sayHello = createGreeter("Hello");


const sayHi = createGreeter("Hi");

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


console.log(sayHi("Jane")); // "Hi, Jane!"

Closures in Loops

As we saw earlier, closures can be tricky in loops when using var :

// Problem with var


function createButtons() {
for (var i = 1; i <= 3; i++) {
const button = document.createElement("button");
button.textContent = `Button ${i}`;
button.addEventListener("click", function() {
console.log(`Button ${i} clicked`); // Always
"Button 4 clicked"
});
document.body.appendChild(button);
}
}

// Solution 1: Use let instead of var


function createButtonsWithLet() {
for (let i = 1; i <= 3; i++) {
const button = document.createElement("button");
button.textContent = `Button ${i}`;
button.addEventListener("click", function() {
console.log(`Button ${i} clicked`); // Correctly
logs Button 1, 2, or 3
});
document.body.appendChild(button);
}
}

// Solution 2: Use an IIFE (Immediately Invoked Function


Expression)
function createButtonsWithIIFE() {
for (var i = 1; i <= 3; i++) {
(function(buttonNumber) {
const button = document.createElement("button");
button.textContent = `Button ${buttonNumber}`;
button.addEventListener("click", function() {
console.log(`Button ${buttonNumber}
clicked`); // Correctly logs Button 1, 2, or 3
});
document.body.appendChild(button);
})(i);
}
}

9.3.2 Practical Applications

Closures are incredibly useful in JavaScript. Here are some common applications:

Data Privacy / Encapsulation

Closures can be used to create private variables and methods:

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;
}
};
}

const account = createBankAccount(100);


console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
account.withdraw(30);
console.log(account.getBalance()); // 120

// The balance variable is not directly accessible


// console.log(account.balance); // undefined

Function Factories

Closures can be used to create functions with pre-configured behavior:

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

const double = multiply(2);


const triple = multiply(3);

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

Memoization

Closures can be used to cache expensive function results:

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;
};
}

// Example: Expensive calculation


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

// 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:

function setupButton(buttonId, message) {


const button = document.getElementById(buttonId);

button.addEventListener("click", function() {
// The event handler forms a closure over the message
variable
alert(message);
});
}

setupButton("btn1", "Hello, World!");


setupButton("btn2", "Button 2 was clicked!");

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

Currying is a technique of transforming a function that takes multiple arguments into a


sequence of functions that each take a single argument:

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;
}

const curriedAdd = curry(add);

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

9.4 The Module Pattern


The module pattern is one of the most common design patterns in JavaScript. It uses
closures to create private and public methods and variables.

Basic Module Pattern

const counterModule = (function() {


// Private variables and functions
let count = 0;

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

Revealing Module Pattern

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:

const revealingModule = (function() {


// Private variables
let privateVar = "I am private";
let publicVar = "I am public";

// Private function
function privateFunction() {
console.log("This is private");
}

// Public function
function publicFunction() {
console.log("This is public");
privateFunction();
}

// Reveal public pointers to private functions and


properties
return {
publicVar: publicVar,
publicFunction: publicFunction,
// We could also rename:
// exposedFunction: privateFunction
};
})();

console.log(revealingModule.publicVar); // "I am public"


revealingModule.publicFunction(); // "This is public" followed
by "This is private"
// console.log(revealingModule.privateVar); // undefined
// revealingModule.privateFunction(); // TypeError:
revealingModule.privateFunction is not a function

Module Augmentation

Modules can be extended or augmented:

const calculatorModule = (function() {


let result = 0;

return {
add: function(num) {
result += num;
return this;
},
subtract: function(num) {
result -= num;
return this;
},
getResult: function() {
return result;
}
};
})();

// Augment the module with new functionality


(function(module) {
module.multiply = function(num) {
const currentResult = module.getResult();
module.add(currentResult * num - currentResult);
return this;
};

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

ES6 Modules vs. Module Pattern

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);
}

export function calculateCircleCircumference(radius) {


return 2 * PI * radius;
}

// main.js
import { calculateCircleArea, calculateCircleCircumference }
from './math.js';

console.log(calculateCircleArea(5)); // ~78.54
console.log(calculateCircleCircumference(5)); // ~31.42

9.5 Memory Management and Garbage Collection


Understanding closures also requires understanding how JavaScript manages memory.

How Garbage Collection Works

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;
}

const myObj = createObject(); // obj is referenced by myObj, so


it's not garbage collected
myObj = null; // Now obj can be garbage collected

Closures and Memory Leaks

Closures can sometimes cause memory leaks if not handled properly:

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

Preventing Memory Leaks

To prevent memory leaks with closures:

1. Be mindful of what your closures capture


2. Remove event listeners when they're no longer needed
3. Avoid creating unnecessary closures in loops
4. Use weak references when appropriate

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);

// Store a reference to the handler for later removal


return function cleanup() {
element.removeEventListener("click", clickHandler);
};
}

const cleanup = setupHandler();

// Later, when the handler is no longer needed


cleanup();

9.6 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• 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

• Lexical Scope: How JavaScript determines variable access based on where


functions are defined in the code

• Closures: Functions that remember their lexical scope even when executed outside
that scope

• How closures are created


• Common applications of closures

• Potential memory issues with closures

• 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.

Exercise 3: The Module Pattern

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.

Exercise 4: Practical Applications

1. Create a function that uses closures to implement currying (converting a function


that takes multiple arguments into a sequence of functions that each take a single
argument).
2. Write a debounce function that limits how often a function can be called.
3. Create a throttle function that ensures a function is called at most once in a
specified time period.
4. Implement a simple event emitter using closures.
5. Create a function that generates unique IDs using closures.

Exercise 5: Advanced Challenge

Create a small library for managing asynchronous tasks that uses closures to track the
state of each task. The library should include:

1. A function to create a new task


2. Methods to start, pause, resume, and cancel tasks
3. Callbacks for task completion, failure, and progress updates
4. A way to chain tasks together
5. Methods to get the current state and progress of a task

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.

Chapter 10: AJAX and Fetch API


Modern web applications need to communicate with servers without reloading the
entire page. This is where AJAX (Asynchronous JavaScript and XML) and the Fetch API
come in. These technologies allow you to make asynchronous HTTP requests, retrieve
data from servers, and update your web pages dynamically. In this chapter, we'll explore
how to use these powerful tools to create responsive and interactive web applications.

10.1 Introduction to AJAX


AJAX stands for Asynchronous JavaScript and XML. Despite the name, it's not limited to
XML data—it can work with any data format, including JSON, HTML, and plain text. AJAX
allows web applications to:

• Send data to a server in the background


• Receive data from a server in the background
• Update parts of a web page without reloading the whole page

How AJAX Works

The basic flow of an AJAX request is:

1. An event occurs in a web page (like a button click)


2. JavaScript creates an XMLHttpRequest object or uses the Fetch API
3. The request is sent to the server
4. The server processes the request
5. The server sends a response back
6. JavaScript processes the response
7. The page content is updated accordingly

Benefits of AJAX

• Improved User Experience: Pages update without full reloads, making


applications feel more responsive
• Reduced Server Load: Only necessary data is transferred, not entire pages
• Separation of Concerns: Data retrieval is separated from presentation
• Asynchronous Processing: Users can continue interacting with the page while
requests are being processed

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.

Basic XHR Request

Here's a simple example of making a GET request:

// Create a new XMLHttpRequest object


const xhr = new XMLHttpRequest();

// Configure the request


xhr.open('GET', 'https://api.example.com/data', true);

// Set up a function to handle the response


xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// Request was successful
console.log('Response:', xhr.responseText);
const data = JSON.parse(xhr.responseText);
// Do something with the data
} else {
// Request failed
console.error('Request failed with status:',
xhr.status);
}
};

// Set up error handling


xhr.onerror = function() {
console.error('Request failed');
};

// Send the request


xhr.send();

XHR Properties and Methods

Important Properties

• readyState : The state of the request (0-4)


• 0: UNSENT - Client has been created, but open() not called yet
• 1: OPENED - open() has been called
• 2: HEADERS_RECEIVED - send() has been called, headers and status are available
• 3: LOADING - Downloading; responseText holds partial data
• 4: DONE - The operation is complete
• status : The HTTP status code (e.g., 200 for success, 404 for not found)
• statusText : The status message (e.g., "OK", "Not Found")
• responseText : The response as text
• responseXML : The response as XML (if applicable)
• response : The response in the format specified by responseType

Important Methods

• open(method, url, async) : Configures the request


• send(data) : Sends the request
• setRequestHeader(header, value) : Sets an HTTP request header
• abort() : Aborts the request
• getAllResponseHeaders() : Returns all response headers
• getResponseHeader(header) : Returns a specific response header
Handling Different Response Types

const xhr = new XMLHttpRequest();


xhr.open('GET', 'https://api.example.com/data', true);

// Specify the response type


xhr.responseType = 'json'; // Can be 'text', 'json', 'blob',
'arraybuffer', 'document'

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();

POST Requests with XHR

const xhr = new XMLHttpRequest();


xhr.open('POST', 'https://api.example.com/submit', true);

// Set the content type header


xhr.setRequestHeader('Content-Type', 'application/json');

// Set up response handling


xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Success:', xhr.responseText);
} else {
console.error('Error:', xhr.statusText);
}
};

// Prepare the data


const data = {
name: 'John Doe',
email: '[email protected]',
message: 'Hello, world!'
};

// Send the request with the data


xhr.send(JSON.stringify(data));
Monitoring Progress

XHR allows you to monitor the progress of requests, which is useful for large file uploads
or downloads:

const xhr = new XMLHttpRequest();


xhr.open('GET', 'https://example.com/large-file.zip', true);
xhr.responseType = 'blob';

// Progress event
xhr.onprogress = function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) *
100;
console.log(`Progress: ${percentComplete.toFixed(2)}%`);

// Update a progress bar


document.getElementById('progress-bar').value =
percentComplete;
}
};

// Load complete event


xhr.onload = function() {
if (xhr.status === 200) {
// Create a URL for the blob
const url = URL.createObjectURL(xhr.response);

// Create a download link


const a = document.createElement('a');
a.href = url;
a.download = 'downloaded-file.zip';
a.click();

// Clean up
URL.revokeObjectURL(url);
}
};

xhr.send();

Handling Timeouts

You can set a timeout for XHR requests:

const xhr = new XMLHttpRequest();


xhr.open('GET', 'https://api.example.com/data', true);
// Set timeout in milliseconds
xhr.timeout = 5000; // 5 seconds

// Timeout handler
xhr.ontimeout = function() {
console.error('Request timed out');
};

xhr.onload = function() {
if (xhr.status === 200) {
console.log('Response:', xhr.responseText);
}
};

xhr.send();

Using Events Instead of Properties

Instead of using properties like onload and onerror , you can use event listeners:

const xhr = new XMLHttpRequest();


xhr.open('GET', 'https://api.example.com/data', true);

// Add event listeners


xhr.addEventListener('load', function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Success:', xhr.responseText);
}
});

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.

10.3.1 Basic Fetch Requests

Making a simple GET request with Fetch:

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);
});

Using async/await for cleaner code:

async function fetchData() {


try {
const response = await fetch('https://api.example.com/
data');

if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}

const data = await response.json();


console.log('Data:', data);
return data;
} catch (error) {
console.error('Fetch error:', error);
}
}
// Call the function
fetchData();

10.3.2 Request and Response Objects

The Fetch API uses Request and Response objects to represent HTTP requests and
responses.

The Request Object

You can create a Request object to configure your fetch request:

// Create a request object


const request = new Request('https://api.example.com/data', {
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
}),
mode: 'cors',
cache: 'default'
});

// Use the request object with fetch


fetch(request)
.then(response => response.json())
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));

The Response Object

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);

// Clone the response (useful if you need to use it


multiple times)
const responseClone = response.clone();

// Return the original response parsed as JSON


return response.json();
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
console.error('Error:', error);
});

10.3.3 Handling Different Data Formats

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));

// Blob data (for images, files, etc.)


fetch('https://example.com/image.jpg')
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
});

// ArrayBuffer (for binary data)


fetch('https://example.com/binary-data')
.then(response => response.arrayBuffer())
.then(buffer => {
// Work with the binary data
const view = new Uint8Array(buffer);
console.log('First byte:', view[0]);
});

// FormData
fetch('https://example.com/form-data')
.then(response => response.formData())
.then(formData => {
console.log('Form field value:', formData.get('field-
name'));
});

POST Requests with Fetch

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);
});

Sending Form Data

// Get form element


const form = document.getElementById('my-form');

// Listen for form submission


form.addEventListener('submit', function(event) {
// Prevent the default form submission
event.preventDefault();

// Create a FormData object from the form


const formData = new FormData(form);

// Send the form data


fetch('https://api.example.com/submit-form', {
method: 'POST',
body: formData
// Note: Don't set Content-Type header when sending
FormData
// The browser will set it automatically with the
correct boundary
})
.then(response => response.json())
.then(result => {
console.log('Form submission result:', result);
})
.catch(error => {
console.error('Form submission error:', error);
});
});

Uploading Files

// Get file input element


const fileInput = document.getElementById('file-input');

// Listen for file selection


fileInput.addEventListener('change', function() {
// Get the selected file
const file = fileInput.files[0];
if (!file) return;

// Create FormData and append the file


const formData = new FormData();
formData.append('file', file);

// Upload the file


fetch('https://api.example.com/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
console.log('File upload result:', result);
})
.catch(error => {
console.error('File upload error:', error);
});
});

Request Configuration Options

The fetch function accepts a second parameter with configuration options:

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.

// Mode (for CORS)


mode: 'cors', // 'no-cors', 'same-origin', 'cors'

// 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

// Signal for aborting the request


signal: abortController.signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

10.3.4 Error Handling

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);
});

Using async/await with try/catch:

async function fetchData() {


try {
const response = await fetch('https://api.example.com/
data');

if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status} ${response.statusText}`);
}

const data = await response.json();


return data;
} catch (error) {
console.error('Fetch error:', error);
// You can also re-throw the error or return a default
value
throw error; // Re-throw
// return []; // Return default value
}
}

Timeout with AbortController

Fetch doesn't have a built-in timeout option, but you can implement one using
AbortController:

function fetchWithTimeout(url, options = {}, timeout = 5000) {


// Create an AbortController instance
const controller = new AbortController();
const { signal } = controller;

// Set up the timeout


const timeoutId = setTimeout(() => controller.abort(),
timeout);

// Add the signal to the fetch options


const fetchOptions = { ...options, signal };

// Make the fetch request


return fetch(url, fetchOptions)
.then(response => {
// Clear the timeout
clearTimeout(timeoutId);
return response;
})
.catch(error => {
// Clear the timeout
clearTimeout(timeoutId);

// Check if the request was aborted due to timeout


if (error.name === 'AbortError') {
throw new Error(`Request timed out after $
{timeout}ms`);
}

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

You can cancel fetch requests using AbortController:

// Create an AbortController
const controller = new AbortController();
const signal = controller.signal;

// Start the fetch


fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log('Data:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch was aborted');
} else {
console.error('Fetch error:', error);
}
});

// Later, to cancel the request


document.getElementById('cancel-
button').addEventListener('click', () => {
controller.abort();
console.log('Fetch aborted');
});

10.4 Cross-Origin Resource Sharing (CORS)


CORS is a security feature implemented by browsers that restricts web pages from
making requests to a different domain than the one that served the web page. This is
known as the same-origin policy.

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

The main CORS headers are:

• Access-Control-Allow-Origin : Specifies which origins can access the


resource
• Access-Control-Allow-Methods : Specifies the allowed HTTP methods
• Access-Control-Allow-Headers : Specifies which headers can be used
• Access-Control-Allow-Credentials : Specifies whether credentials (cookies,
authorization headers) can be included
• Access-Control-Max-Age : Specifies how long the results of a preflight request
can be cached
Simple vs. Preflighted Requests

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.

Handling CORS in Client-Side Code

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

Example of a simple proxy server using Express:

// Server-side code (Node.js with Express)


const express = require('express');
const axios = require('axios');
const app = express();

app.get('/api/proxy', async (req, res) => {


try {
const targetUrl = req.query.url;
const response = await axios.get(targetUrl);
res.json(response.data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

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));

10.5 Building a Simple API Client


Let's put everything together and build a simple, reusable API client using the Fetch API:

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 authorization token


setAuthToken(token) {
this.defaultOptions.headers.Authorization = `Bearer $
{token}`;
}

// Clear authorization token


clearAuthToken() {
delete this.defaultOptions.headers.Authorization;
}

// Make a request with timeout


async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const controller = new AbortController();
const { signal } = controller;

// 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
};

// Convert body to JSON string if it's an object


if (fetchOptions.body && typeof fetchOptions.body
=== 'object') {
fetchOptions.body =
JSON.stringify(fetchOptions.body);
}

// Make the request


const response = await fetch(url, fetchOptions);

// Clear the timeout


clearTimeout(timeoutId);

// Check for HTTP errors


if (!response.ok) {
const errorData = await
response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP
error! Status: ${response.status}`);
}

// Parse the response based on content type


const contentType = response.headers.get('content-
type');
if (contentType &&
contentType.includes('application/json')) {
return await response.json();
} else {
return await response.text();
}
} catch (error) {
// Clear the timeout
clearTimeout(timeoutId);

// Handle abort errors


if (error.name === 'AbortError') {
throw new Error(`Request timed out after $
{timeout}ms`);
}

throw error;
}
}

// Convenience methods for different HTTP methods


async get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method:
'GET' });
}

async post(endpoint, data, options = {}) {


return this.request(endpoint, { ...options, method:
'POST', body: data });
}

async put(endpoint, data, options = {}) {


return this.request(endpoint, { ...options, method:
'PUT', body: data });
}

async patch(endpoint, data, options = {}) {


return this.request(endpoint, { ...options, method:
'PATCH', body: data });
}

async delete(endpoint, options = {}) {


return this.request(endpoint, { ...options, method:
'DELETE' });
}
}

// Usage example
const api = new ApiClient('https://api.example.com');

// Login and get a token


async function login(username, password) {
try {
const result = await api.post('/login', { username,
password });
api.setAuthToken(result.token);
return result;
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}

// Get user data


async function getUserData(userId) {
try {
return await api.get(`/users/${userId}`);
} catch (error) {
console.error('Failed to get user data:', error);
throw error;
}
}

// Create a new post


async function createPost(title, content) {
try {
return await api.post('/posts', { title, content });
} catch (error) {
console.error('Failed to create post:', error);
throw error;
}
}

// Example usage
async function main() {
try {
await login('user123', 'password123');
const userData = await getUserData(123);
console.log('User data:', userData);

const newPost = await createPost('Hello World',


'This is my first post!');
console.log('New post created:', newPost);
} catch (error) {
console.error('Error in main flow:', error);
}
}

main();
10.6 Chapter Summary and Exercises

Summary

In this chapter, we've covered:

• Introduction to AJAX: Understanding asynchronous requests and their benefits


• XMLHttpRequest: The traditional way to make AJAX requests
• Fetch API: The modern approach to making HTTP requests
• Basic requests and handling responses
• Working with different data formats
• Error handling and request cancellation
• Cross-Origin Resource Sharing (CORS): Understanding and working with cross-
origin restrictions
• Building an API Client: Creating a reusable client for working with APIs

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

Exercise 1: XMLHttpRequest Basics

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.

Exercise 2: Fetch API Basics

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).

Exercise 4: API Interaction

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.

Exercise 5: Practical Application

Build a simple "Todo List" application that:

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.

Chapter 11: Intermediate Code Examples


This chapter provides practical code examples that integrate the concepts covered in the
intermediate chapters (Chapters 6-10). These examples demonstrate how to combine
DOM manipulation, events, advanced objects, closures, and asynchronous programming
to build more complex features.
Example 1: Interactive To-Do List
This example combines DOM manipulation, event handling (including delegation), and
closures to create a functional to-do list.

<!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");

// Function to create a new list item


function createTodoItem(text) {
const li = document.createElement("li");

const checkbox =
document.createElement("input");
checkbox.type = "checkbox";
checkbox.classList.add("toggle-complete");

const span = document.createElement("span");


span.textContent = text;

const deleteBtn =
document.createElement("button");
deleteBtn.textContent = "Delete";
deleteBtn.classList.add("delete-btn");

li.appendChild(checkbox);
li.appendChild(span);
li.appendChild(deleteBtn);

return li;
}

// Function to add a new todo


function addTodo() {
const text = newTodoInput.value.trim();
if (text) {
const newItem = createTodoItem(text);
todoList.appendChild(newItem);
newTodoInput.value = ""; // Clear input
}
}

// Event listener for adding a todo


addBtn.addEventListener("click", addTodo);
newTodoInput.addEventListener("keypress",
function(event) {
if (event.key === "Enter") {
addTodo();
}
});

// Event delegation for handling clicks on list


items
todoList.addEventListener("click", function(event) {
const target = event.target;
const listItem = target.closest("li");

if (!listItem)
return; // Clicked outside a list item

// Handle checkbox toggle


if (target.classList.contains("toggle-
complete")) {
listItem.classList.toggle("completed");
}

// Handle delete button


if (target.classList.contains("delete-btn")) {
listItem.remove();
}
});

})(); // IIFE to encapsulate the code


</script>
</body>
</html>

Concepts Used: - DOM Manipulation: getElementById , createElement ,


appendChild , textContent , value , remove - Events: addEventListener ,
click , keypress , event object ( event.target , event.key ) - Event Delegation:
Attaching a single listener to the ul element - Closures: The IIFE creates a closure,
encapsulating the variables and functions. - Block Scope: const is used for variables.

Example 2: Simple API Client Class


This example builds upon the Fetch API chapter, creating a reusable class to interact
with a REST API.

class SimpleApiClient {
constructor(baseUrl) {
if (!baseUrl) {
throw new Error("Base URL is required for API
Client");
}
this.baseUrl = baseUrl;
}

async #request(endpoint, options = {}) {


const url = `${this.baseUrl}${endpoint}`;
const controller = new AbortController();
const signal = controller.signal;
const timeout = options.timeout || 8000; // Default
timeout 8 seconds

const timeoutId = setTimeout(() => controller.abort(),


timeout);

const fetchOptions = {
method: options.method || "GET",
headers: {
"Content-Type": "application/json",
...options.headers,
},
signal,
...options,
};

// Only add body if method is not GET or HEAD


if (fetchOptions.method !== "GET" &&
fetchOptions.method !== "HEAD" && options.body) {
fetchOptions.body = JSON.stringify(options.body);
}

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}`);
}

// Handle empty response body for certain status


codes (e.g., 204 No Content)
if (response.status === 204) {
return null;
}

return await response.json();


} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
throw new Error(`Request timed out after $
{timeout}ms`);
}
throw error;
}
}

// Public methods
get(endpoint, options = {}) {
return this.#request(endpoint, { ...options, method:
"GET" });
}

post(endpoint, body, options = {}) {


return this.#request(endpoint, { ...options, method:
"POST", body });
}

put(endpoint, body, options = {}) {


return this.#request(endpoint, { ...options, method:
"PUT", body });
}

delete(endpoint, options = {}) {


return this.#request(endpoint, { ...options, method:
"DELETE" });
}
}

// --- Usage Example ---


// Assuming JSONPlaceholder API
const apiClient = new SimpleApiClient("https://
jsonplaceholder.typicode.com");

async function testApiClient() {


try {
console.log("Fetching posts...");
const posts = await apiClient.get("/posts?_limit=5");
console.log("Posts:", posts);

console.log("\nFetching post with ID 1...");


const post1 = await apiClient.get("/posts/1");
console.log("Post 1:", post1);

console.log("\nCreating a new post...");


const newPost = {
title: "foo",
body: "bar",
userId: 1,
};
const createdPost = await apiClient.post("/posts",
newPost);
console.log("Created Post:", createdPost);

console.log("\nUpdating post with ID 1...");


const updatedData = { title: "Updated Title" };
const updatedPost = await apiClient.put(`/posts/$
{createdPost.id}`, { ...createdPost, ...updatedData });
console.log("Updated Post:", updatedPost);

console.log("\nDeleting post with ID 1...");


await apiClient.delete(`/posts/${createdPost.id}`);
console.log(`Post ${createdPost.id} deleted
successfully.`);

} catch (error) {
console.error("API Client Test Failed:", error);
}
}

testApiClient();

Concepts Used: - Advanced Objects: ES6 Classes, private methods ( #request ),


constructor - Fetch API: Making GET, POST, PUT, DELETE requests - Promises: async /
await for handling asynchronous operations - Error Handling: try / catch , checking
response.ok , custom error messages - Closures: The methods of the class instance
form closures over the baseUrl . - AbortController: Implementing request timeouts.

Example 3: Debounced Search Input


This example implements a search input that only triggers an API call after the user has
stopped typing for a specified duration.

<!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);
};
}

const searchInput = document.getElementById("search-


input");
const resultsList = document.getElementById("results-
list");

// Function to perform the search (using Wikipedia API)


async function performSearch(query) {
if (!query) {
resultsList.innerHTML = "";
return;
}

const endpoint = `https://en.wikipedia.org/w/


api.php?action=opensearch&search=${encodeURIComponent(query)}
&limit=10&namespace=0&format=json&origin=*`;

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>";
}
}

// Function to display results


function displayResults(data) {
const titles = data[1];
const links = data[3];
resultsList.innerHTML = ""; // Clear previous
results

if (titles.length === 0) {
resultsList.innerHTML = "<li>No results found.</
li>";
return;
}

titles.forEach((title, index) => {


const li = document.createElement("li");
const a = document.createElement("a");
a.href = links[index];
a.textContent = title;
a.target = "_blank"; // Open in new tab
li.appendChild(a);
resultsList.appendChild(li);
});
}

// Create a debounced version of the search function


const debouncedSearch = debounce(performSearch,
500); // 500ms delay

// Add event listener to the input


searchInput.addEventListener("input", (event) => {
debouncedSearch(event.target.value);
});
</script>
</body>
</html>

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.

Example 4: Image Gallery with Lightbox


This example creates a simple image gallery where clicking a thumbnail opens a larger
version in a lightbox overlay.

<!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>

<!-- The Lightbox -->


<div id="myLightbox" class="lightbox">
<span class="lightbox-close">&times;</span>
<img class="lightbox-content" id="lightboxImage">
</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");

// Sample image URLs (replace with actual API call


if needed)
const imageUrls = [
"https://via.placeholder.com/600/92c952",
"https://via.placeholder.com/600/771796",
"https://via.placeholder.com/600/24f355",
"https://via.placeholder.com/600/d32776",
"https://via.placeholder.com/600/f66b97",
"https://via.placeholder.com/600/56a8c2"
];

// Populate the gallery


imageUrls.forEach(url => {
const img = document.createElement("img");
img.src = url.replace("600", "150"); // Use
smaller thumbnail version
img.dataset.fullSrc = url; // Store full image
URL
galleryContainer.appendChild(img);
});
// Function to open the lightbox
function openLightbox(src) {
lightboxImage.src = src;
lightbox.style.display = "flex";
}

// Function to close the lightbox


function closeLightbox() {
lightbox.style.display = "none";
lightboxImage.src = ""; // Clear image
}

// Event delegation for gallery clicks


galleryContainer.addEventListener("click",
function(event) {
if (event.target.tagName === "IMG") {
openLightbox(event.target.dataset.fullSrc);
}
});

// Event listener for close button


closeBtn.addEventListener("click", closeLightbox);

// Close lightbox when clicking outside the image


lightbox.addEventListener("click", function(event) {
if (event.target === lightbox) {
closeLightbox();
}
});

// Close lightbox with Escape key


document.addEventListener("keydown",
function(event) {
if (event.key === "Escape" &&
lightbox.style.display === "flex") {
closeLightbox();
}
});

})();
</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"
}

// Register an event listener


on(eventName, listener) {
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(listener);
}

// Remove an event listener


off(eventName, listenerToRemove) {
if (!this._events[eventName]) {
return;
}
this._events[eventName] =
this._events[eventName].filter(
listener => listener !== listenerToRemove
);
}

// 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);
}
});
}

// Register a one-time event listener


once(eventName, listener) {
const onceWrapper = (...args) => {
listener.apply(this, args);
this.off(eventName, onceWrapper); // Remove after
first execution
};
this.on(eventName, onceWrapper);
}
}

// --- Usage Example ---


const emitter = new EventEmitter();

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" });

console.log("\nEmitting 'urgent' event...");


emitter.emit("urgent", "System critical!");

console.log("\nEmitting 'special' event (1st time)...");


emitter.emit("special", { info: "First payload" });

console.log("\nEmitting 'special' event (2nd time)...");


emitter.emit("special", { info: "Second
payload" }); // Listener should be gone

// Remove a listener
console.log("\nRemoving 'data' listener...");
emitter.off("data", handleData);

console.log("\nEmitting 'data' event again...");


emitter.emit("data", { id: 2, value: "After removal" }); //
Should not log anything

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.

Chapter 12: Design Patterns in JavaScript


Design patterns are reusable solutions to commonly occurring problems within a given
context in software design. They are not specific algorithms or code snippets but rather
templates for how to solve a problem that can be used in many different situations. In
JavaScript, understanding and applying design patterns can lead to more maintainable,
scalable, and robust code. This chapter explores several common design patterns
relevant to JavaScript development.

12.1 Introduction to Design Patterns


Design patterns provide a shared vocabulary and proven solutions for common software
design challenges. They originated from the work of the "Gang of Four" (GoF) in their
book "Design Patterns: Elements of Reusable Object-Oriented Software."

Why Use Design Patterns?

• Reusability: Patterns provide proven solutions that can be adapted.


• Maintainability: Code based on patterns is often easier to understand and modify.
• Communication: Patterns provide a common language for developers.
• Efficiency: They help avoid reinventing the wheel for common problems.
• Scalability: Patterns often lead to more flexible and extensible architectures.

Categories of Design Patterns

Design patterns are typically categorized into three main groups:

1. Creational Patterns: Deal with object creation mechanisms, trying to create


objects in a manner suitable to the situation.
2. Structural Patterns: Deal with object composition, assembling objects and classes
into larger structures while keeping these structures flexible and efficient.
3. Behavioral Patterns: Deal with communication between objects, identifying
common communication patterns and realizing these patterns.

12.2 Creational Patterns


Creational patterns focus on how objects are created.

12.2.1 Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global
point of access to it.

Use Case: Managing a shared resource like a database connection, configuration


settings, or a logger.

const Singleton = (function() {


let instance;

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();

console.log(instance1 === instance2); // true

instance1.publicMethod();
// Output:
// Public method called
// Private method called

console.log(instance1.getPrivateVariable()); // "I am private"


// console.log(instance1.privateVariable); // undefined
(private)

ES6 Class Version:

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;
}
}

const s1 = new SingletonClass();


const s2 = SingletonClass.getInstance();
const s3 = new SingletonClass();

console.log(s1 === s2); // true


console.log(s2 === s3); // true
console.log(s1.getData()); // "Data: Secret"

12.2.2 Factory Pattern

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.

// Base class (or constructor)


function Employee(name, type) {
this.name = name;
this.type = type;
}

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 = [];

employees.push(factory.create("John Doe", 1));


employees.push(factory.create("Jane Smith", 2));
employees.push(factory.create("Peter Jones", 1));

employees.forEach(emp => emp.say());


// Output:
// I am John Doe and I am a Developer
// I am Jane Smith and I am a Tester
// I am Peter Jones and I am a Developer
ES6 Class Version:

class Employee {
constructor(name, role) {
this.name = name;
this.role = role;
}

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

class Developer extends Employee {


constructor(name) {
super(name, "Developer");
}
}

class Tester extends Employee {


constructor(name) {
super(name, "Tester");
}
}

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");

dev.introduce(); // Hi, I'm Alice, a Developer.


tester.introduce(); // Hi, I'm Bob, a Tester.

12.2.3 Constructor Pattern

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}.`);
};
}

const person1 = new Person("Alice", 30);


const person2 = new Person("Bob", 25);

person1.greet(); // Hello, my name is Alice and I am 30.


person2.greet(); // Hello, my name is Bob and I am 25.

// ES6 Class
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}

displayInfo() {
console.log(`Car: ${this.make} ${this.model}`);
}
}

const car1 = new Car("Toyota", "Camry");


const car2 = new Car("Honda", "Civic");

car1.displayInfo(); // Car: Toyota Camry


car2.displayInfo(); // Car: Honda Civic

12.3 Structural Patterns


Structural patterns focus on how objects and classes are composed to form larger
structures.

12.3.1 Module Pattern

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.

const CalculatorModule = (function() {


let result = 0; // Private state

function add(x) { result += x; }


function subtract(x) { result -= x; }
function getResult() { return result; }

// Reveal public interface


return {
add: add,
subtract: subtract,
getResult: getResult
};
})();

CalculatorModule.add(10);
CalculatorModule.subtract(3);
console.log(CalculatorModule.getResult()); // 7
// console.log(CalculatorModule.result); // undefined (private)

12.3.2 Facade Pattern

The Facade pattern provides a simplified interface to a complex subsystem (a library,


framework, or set of classes).

Use Case: Simplifying interaction with a complex system, providing a higher-level


interface.

// 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

12.3.3 Decorator Pattern

The Decorator pattern allows behavior to be added to an individual object, dynamically,


without affecting the behavior of other objects from the same class.

Use Case: Adding features or responsibilities to objects dynamically and transparently.

// 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

myCoffee = new MilkDecorator(myCoffee);


console.log(myCoffee.description(), "Cost:",
myCoffee.cost()); // Simple Coffee, Milk Cost: 7

myCoffee = new SugarDecorator(myCoffee);


console.log(myCoffee.description(), "Cost:",
myCoffee.cost()); // Simple Coffee, Milk, Sugar Cost: 8

let anotherCoffee = new SugarDecorator(new Coffee());


console.log(anotherCoffee.description(), "Cost:",
anotherCoffee.cost()); // Simple Coffee, Sugar Cost: 6

12.3.4 Adapter Pattern

The Adapter pattern allows objects with incompatible interfaces to collaborate.

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;
}
}
}

// New Interface (Adaptee)


class NewCalculator {
add(term1, term2) {
return term1 + term2;
}
subtract(term1, term2) {
return term1 - term2;
}
}

// Adapter
class CalculatorAdapter {
constructor() {
this.newCalculator = new NewCalculator();
}

operations(term1, term2, operation) {


switch (operation) {
case 'add': return this.newCalculator.add(term1,
term2);
case 'sub': return
this.newCalculator.subtract(term1, term2);
default: return NaN;
}
}
}

// Usage
const oldCalc = new OldCalculator();
console.log("Old Calc Add:", oldCalc.operations(10, 5,
'add')); // 15

const newCalc = new NewCalculator();


// console.log(newCalc.operations(10, 5, 'add')); // Error:
operations is not a function
console.log("New Calc Add:", newCalc.add(10, 5)); // 15

const adaptedCalc = new CalculatorAdapter();


console.log("Adapted Calc Add:", adaptedCalc.operations(10, 5,
'add')); // 15
console.log("Adapted Calc Sub:", adaptedCalc.operations(10, 5,
'sub')); // 5

12.4 Behavioral Patterns


Behavioral patterns focus on algorithms and the assignment of responsibilities between
objects.

12.4.1 Observer Pattern

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.

Use Case: Implementing event handling systems, keeping UI components synchronized


with data models.

// 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();

const observer1 = new Observer("Observer 1");


const observer2 = new Observer("Observer 2");
const observer3 = new Observer("Observer 3");

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.subscribe(observer3);

console.log("Notifying all observers...");


subject.notify({ message: "First update!" });
// Output:
// Observer 1 received update: { message: 'First update!' }
// Observer 2 received update: { message: 'First update!' }
// Observer 3 received update: { message: 'First update!' }

console.log("\nUnsubscribing Observer 2...");


subject.unsubscribe(observer2);

console.log("\nNotifying remaining observers...");


subject.notify({ message: "Second update after unsubscribe!" });
// Output:
// Observer 1 received update: { message: 'Second update after
unsubscribe!' }
// Observer 3 received update: { message: 'Second update after
unsubscribe!' }

This is similar to the EventEmitter built in Example 5 of Chapter 11.

12.4.2 Command Pattern

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}`);
}
}

// Command Interface (Implicit)


class Command {
constructor(calculator, operator, value) {
this.calculator = calculator;
this.operator = operator;
this.value = value;
}
execute() {
this.calculator.operation(this.operator, this.value);
}
undo() {
const oppositeOperator = {
'+': '-', '-': '+', '*': '/', '/': '*'
}[this.operator];
this.calculator.operation(oppositeOperator, this.value);
}
}

// 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();

history.execute(new Command(calculator, '+', 10)); // Current


value: 10
history.execute(new Command(calculator, '*', 2)); // Current
value: 20
history.execute(new Command(calculator, '-', 5)); // Current
value: 15

console.log("\n--- Undoing ---");


history.undo(); // Current value: 20
history.undo(); // Current value: 10

console.log("\n--- Redoing ---");


history.redo(); // Current value: 20
history.redo(); // Current value: 15

12.4.3 Strategy Pattern

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.

Use Case: Selecting an algorithm at runtime, avoiding complex conditional statements


for different behaviors.

// 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);
}
}

// Strategy Interface (Implicit)


class ShippingStrategy {
calculate(packageDetails) {
throw new
Error("Calculate method must be implemented by concrete
strategy.");
}
}

// 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
}
}

class UPSStrategy extends ShippingStrategy {


calculate(packageDetails) {
// UPS calculation logic
console.log("Calculating using UPS strategy...");
return packageDetails.weight * 2.2 + 7; // Example
calculation
}
}

class USPSStrategy extends ShippingStrategy {


calculate(packageDetails) {
// USPS calculation logic
console.log("Calculating using USPS strategy...");
return packageDetails.weight * 1.8 + 3; // 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.

12.6 Chapter Summary and Exercises

Summary

In this chapter, we explored several key design patterns applicable to JavaScript:

• Creational Patterns: Singleton, Factory, Constructor.


• Structural Patterns: Module, Facade, Decorator, Adapter.
• Behavioral Patterns: Observer, Command, Strategy.

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

Exercise 1: Singleton Logger

Create a Logger singleton class that provides methods like log(message) ,


warn(message) , and error(message) . Ensure only one instance of the logger exists
throughout the application.

Exercise 2: UI Component Factory

Implement a UIComponentFactory that can create different types of UI components


(e.g., Button , Input , Checkbox ) based on a type string. Each component should
have a render() method.

Exercise 3: Configuration Facade

Imagine a complex configuration system with multiple sources (environment variables,


config files, database). Create a ConfigFacade that provides a simple interface like
getConfig(key) to retrieve configuration values, hiding the complexity of where the
configuration comes from.

Exercise 4: Data Validation Strategy

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.

Exercise 5: Undo/Redo Text Editor

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.

Chapter 13: Modules and Bundlers


Modern JavaScript applications are rarely built as a single monolithic file. Instead,
they're composed of multiple modules that work together. This chapter explores
JavaScript's module systems and the tools used to bundle these modules for
production.
13.1 Introduction to JavaScript Modules
A module is a reusable piece of code that encapsulates implementation details and
exposes a public API. Modules help organize code, prevent global namespace pollution,
and enable code reuse across applications.

Benefits of Using Modules

• Maintainability: Smaller, focused files are easier to understand and maintain


• Namespacing: Avoid global namespace pollution and naming conflicts
• Reusability: Modules can be shared across different parts of an application or
across projects
• Dependency Management: Clearly define relationships between different parts of
your code
• Encapsulation: Hide implementation details and expose only what's necessary

13.2 Module Patterns in JavaScript


JavaScript's module systems have evolved significantly over time. Let's explore the
different approaches, from historical patterns to modern standards.

13.2.1 IIFE Module Pattern

Before ES6 modules, developers used Immediately Invoked Function Expressions (IIFEs)
to create modules:

// IIFE Module Pattern


const counterModule = (function() {
// Private variables and functions
let count = 0;

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

13.2.2 CommonJS Modules

CommonJS is a module format primarily used in Node.js. It uses require() to import


modules and module.exports or exports to export functionality:

// math.js (CommonJS module)


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

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

// Export specific functions


exports.add = add;
exports.subtract = subtract;

// Or replace the entire exports object


module.exports = {
add: add,
subtract: subtract,
multiply: function(a, b) {
return a * b;
}
};

// main.js (using the CommonJS module)


const math = require('./math');

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)

AMD was designed for browsers, allowing modules to be loaded asynchronously:

// Define a module with dependencies


define(['jquery', 'lodash'], function($, _) {
// Module code here
function initialize() {
console.log('Module initialized');
}

// Return public API


return {
init: initialize
};
});

// Usage
require(['myModule'], function(myModule) {
myModule.init();
});

13.2.4 UMD (Universal Module Definition)

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';
}

// Return public API


return {
myMethod: myMethod
};
}));
13.3 ES6 Modules
ES6 (ES2015) introduced a standardized module system for JavaScript. This is now the
recommended approach for organizing JavaScript code.

13.3.1 Basic Syntax

ES6 modules use import and export statements:

// math.js
export function add(a, b) {
return a + b;
}

export function subtract(a, b) {


return a - b;
}

export const PI = 3.14159;

// Private function (not exported)


function square(x) {
return x * x;
}

// 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';

// Import all exports as a namespace object


import * as math 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

13.3.2 Named vs Default Exports

ES6 modules support both named and default exports:

// Named exports
export const name = 'John';
export function sayHello() {
console.log(`Hello, ${name}!`);
}

// Default export (only one per module)


export default class Person {
constructor(name) {
this.name = name;
}

greet() {
console.log(`Hi, I'm ${this.name}`);
}
}

// Importing named and default exports


import Person, { name, sayHello } from './person.js';

sayHello(); // "Hello, John!"

const person = new Person('Jane');


person.greet(); // "Hi, I'm Jane"

13.3.3 Dynamic Imports

ES2020 introduced dynamic imports, allowing modules to be loaded on demand:

// Static import (evaluated at load time)


import { add } from './math.js';

// Dynamic import (returns a Promise, evaluated at runtime)


async function loadMathModule() {
try {
const math = await import('./math.js');
console.log(math.add(5, 3)); // 8
} catch (error) {
console.error('Error loading module:', error);
}
}

// Or using Promise syntax


button.addEventListener('click', () => {
import('./math.js')
.then(math => {
console.log(math.add(5, 3)); // 8
})
.catch(error => {
console.error('Error loading module:', error);
});
});

13.3.4 Module Features and Limitations

ES6 modules have several important characteristics:

1. Strict Mode: Modules automatically run in strict mode


2. Lexical Top-Level Scope: Variables defined at the top level are scoped to the
module, not global
3. Single Instance: Modules are singletons; they're only executed once, no matter
how many times they're imported
4. Hoisted Imports: Import statements are hoisted and processed before the rest of
the code
5. Static Structure: The import/export structure is static and determined at compile
time (except for dynamic imports)
6. No Conditional Imports: You can't use static imports inside conditional
statements

// Example of module features

// 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

13.4 Module Bundlers


Module bundlers are tools that process modules and their dependencies, combining
them into one or more optimized bundles for the browser.

13.4.1 Why Use a Bundler?

• Browser Compatibility: Not all browsers support ES modules natively


• Performance: Reduce the number of HTTP requests by combining files
• Optimization: Minify code, tree-shake unused code, and apply other optimizations
• Preprocessing: Transform code (e.g., TypeScript, JSX) and assets (e.g., CSS,
images)
• Development Experience: Enable features like hot module replacement

13.4.2 Popular Bundlers

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

Parcel is known for its zero-configuration approach:


# Install Parcel
npm install -g parcel-bundler

# Bundle an application
parcel index.html

esbuild

esbuild is a newer bundler focused on speed:

// 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';

export default defineConfig({


plugins: [react()],
build: {
outDir: 'dist',
minify: 'terser'
}
});

13.4.3 Common Bundler Features

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';

// Dynamic import for code splitting


const LazyComponent = React.lazy(() => import('./
LazyComponent'));

function App() {
return (
<div>
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
</div>
);
}

Tree Shaking

Tree shaking is a technique to eliminate unused code:

// utils.js
export function used() {
console.log('This function is used');
}

export function unused() {


console.log('This function is never imported');
}

// main.js
import { used } from './utils';
used(); // Only the 'used' function will be included in the
bundle

Hot Module Replacement (HMR)

HMR allows modules to be updated at runtime without a full page reload:

// Webpack HMR example


if (module.hot) {
module.hot.accept('./component', () => {
console.log('Component updated');
// Re-render the component
});
}
13.5 Setting Up a Modern JavaScript Project
Let's walk through setting up a modern JavaScript project with modules and a bundler.

13.5.1 Project Initialization

# Create a new directory


mkdir my-js-project
cd my-js-project

# Initialize npm
npm init -y

# Install webpack and related tools


npm install --save-dev webpack webpack-cli webpack-dev-server

# Install babel for transpiling


npm install --save-dev @babel/core @babel/preset-env babel-
loader

# Install other useful tools


npm install --save-dev css-loader style-loader html-webpack-
plugin

13.5.2 Project Structure

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

13.5.3 Configuration Files

webpack.config.js

const path = require('path');


const HtmlWebpackPlugin = require('html-webpack-plugin');

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"
}
}

13.5.4 Sample Application

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

export function add(a, b) {


return a + b;
}

export function subtract(a, b) {


return a - b;
}

export default function multiply(a, b) {


return a * b;
}
src/components/Counter.js

export default class Counter {


constructor(initialValue = 0) {
this.count = initialValue;
this.element = document.createElement('div');
this.render();
}

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);

// Add a paragraph with some calculated value


const paragraph = document.createElement('p');
paragraph.textContent = `5 + 3 = ${add(5, 3)}`;
app.appendChild(paragraph);

// Add the counter component


const counter = new Counter(10);
app.appendChild(counter.getElement());
});

13.5.5 Running the Project

# Start development server


npm start

# Build for production


npm run build
13.6 Advanced Module Techniques

13.6.1 Circular Dependencies

Circular dependencies occur when module A imports from module B, and module B
imports from module A:

// moduleA.js
import { functionB } from './moduleB.js';

export function functionA() {


console.log('Function A called');
functionB();
}

// moduleB.js
import { functionA } from './moduleA.js';

export function functionB() {


console.log('Function B called');
// functionA(); // This would create an infinite loop
}

// main.js
import { functionA } from './moduleA.js';
functionA();

Circular dependencies can lead to unexpected behavior and should generally be avoided
by restructuring your code.

13.6.2 Module Augmentation

Sometimes you might want to extend or modify an existing module:

// 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"

13.6.3 Module Side Effects

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';

// No exports needed for a side-effect module

// main.js
import './side-effect.js'; // The side effect runs when imported

13.6.4 Module Patterns with ES6 Modules

You can implement traditional module patterns using ES6 modules:

// 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];
}
}

export default new Singleton();

// 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']

13.7 Chapter Summary and Exercises

Summary

In this chapter, we explored JavaScript modules and bundlers:

• 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

Understanding modules and bundlers is essential for modern JavaScript development.


They help organize code, manage dependencies, and optimize applications for
production.

Exercises

Exercise 1: Basic ES6 Modules

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.

Exercise 2: Module Organization

1. Create a simple application with multiple modules organized in a logical directory


structure.
2. Implement at least three different types of modules: utility functions, UI
components, and data models.
3. Use both default and named exports appropriately.
4. Create an entry point that imports and uses these modules.
Exercise 3: Webpack Configuration

1. Set up a basic Webpack configuration for a small project.


2. Configure loaders for JavaScript (with Babel) and CSS.
3. Add plugins for HTML generation and code minification.
4. Implement development and production configurations.
5. Test the build process and verify the output.

Exercise 4: Dynamic Imports

1. Create an application that uses dynamic imports to load modules on demand.


2. Implement a UI that allows users to trigger the loading of different modules.
3. Add loading indicators while modules are being fetched.
4. Handle errors that might occur during module loading.

Exercise 5: Advanced Project

Create a modular calculator application with the following features:

1. A core module with basic arithmetic operations.


2. Separate modules for advanced operations (scientific, statistical, etc.).
3. A UI module for rendering the calculator interface.
4. Use dynamic imports to load advanced operation modules only when needed.
5. Implement a plugin system that allows adding new operation modules.
6. Bundle the application using Webpack or another bundler of your choice.
7. Implement code splitting to optimize loading times.

Chapter 14: Promises and Async/Await


JavaScript is a single-threaded language, which means it can only execute one piece of
code at a time. However, many operations in web development are asynchronous, such
as fetching data from a server, reading files, or waiting for user input. This chapter
explores JavaScript's mechanisms for handling asynchronous operations, focusing on
Promises and the async/await syntax.

14.1 Understanding Asynchronous JavaScript


Before diving into Promises and async/await, it's important to understand why
asynchronous programming is necessary in JavaScript.
14.1.1 Synchronous vs. Asynchronous Code

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

14.1.2 The Problem with Callbacks

Traditionally, asynchronous operations in JavaScript were handled using callbacks—


functions passed as arguments to be executed later:

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);
}

fetchData((error, data) => {


if (error) {
console.error("Error:", error);
} else {
console.log("Data:", data);
}
});

console.log("Fetching data...");

// Output:
// Fetching data...
// Data: { id: 1, name: "John Doe" } - appears after 1 second

While callbacks work, they can lead to several issues:

1. Callback Hell: Nested callbacks become difficult to read and maintain


2. Error Handling: Error propagation across multiple callbacks is complex
3. Control Flow: Managing sequences of asynchronous operations is challenging
4. Trust Issues: Callbacks might be called multiple times or not at all

Here's an example of "callback hell":

fetchUserData(userId, (error, userData) => {


if (error) {
console.error("Error fetching user:", error);
return;
}

fetchUserPosts(userData.id, (error, posts) => {


if (error) {
console.error("Error fetching posts:", error);
return;
}

fetchPostComments(posts[0].id, (error, comments) => {


if (error) {
console.error("Error fetching comments:",
error);
return;
}

// Finally, we have all the data we need


displayUserData(userData, posts, comments);
});
});
});
14.2 Promises
Promises were introduced in ES6 (ES2015) to address the issues with callbacks. A
Promise is an object representing the eventual completion or failure of an asynchronous
operation.

14.2.1 Promise Basics

A Promise can be in one of three states:

1. Pending: Initial state, neither fulfilled nor rejected


2. Fulfilled: The operation completed successfully
3. Rejected: The operation failed

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)");
});

14.2.2 Chaining Promises

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:

const promise1 = Promise.resolve(3);


const promise2 = new Promise((resolve) => setTimeout(() =>
resolve(42), 100));
const promise3 = fetch('https://jsonplaceholder.typicode.com/
todos/1')
.then(response => response.json());

Promise.all([promise1, promise2, promise3])


.then(values => {
console.log("All promises fulfilled:", values);
// values is an array containing the fulfillment values
of each promise
// [3, 42, {userId: 1, id: 1, title: "...", completed:
false}]
})
.catch(error => {
console.error("At least one promise rejected:", error);
});

Promise.race()

Promise.race() returns a Promise that fulfills or rejects as soon as one of the input
Promises fulfills or rejects:

const promise1 = new Promise((resolve) => setTimeout(() =>


resolve("Fast"), 100));
const promise2 = new Promise((resolve) => setTimeout(() =>
resolve("Slow"), 500));

Promise.race([promise1, promise2])
.then(value => {
console.log("Fastest promise won:", value); // "Fast"
})
.catch(error => {
console.error("Fastest promise rejected:", error);
});
Promise.allSettled()

Promise.allSettled() (introduced in ES2020) returns a Promise that fulfills when


all input Promises have settled, regardless of whether they fulfilled or rejected:

const promise1 = Promise.resolve(3);


const promise2 = Promise.reject("Error");
const promise3 = new Promise((resolve) => setTimeout(() =>
resolve(42), 100));

Promise.allSettled([promise1, promise2, promise3])


.then(results => {
console.log("All promises settled:", results);
// results is an array of objects describing the outcome
of each promise
// [
// { status: "fulfilled", value: 3 },
// { status: "rejected", reason: "Error" },
// { status: "fulfilled", value: 42 }
// ]
});

Promise.any()

Promise.any() (introduced in ES2021) returns a Promise that fulfills as soon as one of


the input Promises fulfills, or rejects if all input Promises reject:

const promise1 = Promise.reject("Error 1");


const promise2 = new Promise((resolve) => setTimeout(() =>
resolve("Success"), 200));
const promise3 = Promise.reject("Error 3");

Promise.any([promise1, promise2, promise3])


.then(value => {
console.log("At least one promise fulfilled:",
value); // "Success"
})
.catch(error => {
console.error("All promises rejected:", error);
// error is an AggregateError containing all rejection
reasons
});

14.2.4 Error Handling in Promises

Proper error handling is crucial when working with Promises:


fetchData()
.then(data => {
// This might throw an error
const processedData = processData(data);
return processedData;
})
.then(processedData => {
// This won't run if the previous step threw an error
displayData(processedData);
})
.catch(error => {
// This catches errors from any previous step in the
chain
console.error("Error in the promise chain:", error);
});

You can also handle errors at specific points in the chain:

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

The async keyword is used to declare an asynchronous function that returns a


Promise:

async function fetchUserData() {


// This function automatically returns a Promise
return { id: 1, name: "John Doe" };
}

// 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:

async function displayUserData() {


try {
const user = await fetchUserData(); // Waits for the
Promise to resolve
console.log("User:", user);
} catch (error) {
console.error("Error:", error);
}
}

displayUserData();

14.3.2 Converting Promise Chains to Async/Await

Let's rewrite our earlier Promise chain example using async/await:

async function getUserDataWithPosts() {


try {
const user = await fetchUser(1);
console.log("User:", user);

const posts = await fetchPosts(user.id);


console.log("Posts:", posts);

const comments = await fetchComments(posts[0].id);


console.log("Comments:", comments);

return { user, posts, comments };


} catch (error) {
console.error("Error:", error);
throw error; // Re-throw the error if needed
}
}

// Call the async function


getUserDataWithPosts()
.then(result => {
console.log("All data:", result);
})
.catch(error => {
console.error("Caught error:", error);
});

14.3.3 Parallel Execution with Async/Await

While sequential execution is straightforward with async/await, you can also run
operations in parallel:

async function getDataInParallel() {


try {
// Start all fetch operations in parallel
const userPromise = fetchUser(1);
const postsPromise = fetchPosts(1);
const commentsPromise = fetchComments(1);

// Wait for all promises to resolve


const user = await userPromise;
const posts = await postsPromise;
const comments = await commentsPromise;

return { user, posts, comments };


} catch (error) {
console.error("Error:", error);
throw error;
}
}

You can also use Promise.all with async/await:

async function getDataInParallel() {


try {
// Wait for all promises to resolve simultaneously
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);

return { user, posts, comments };


} catch (error) {
console.error("Error:", error);
throw error;
}
}

14.3.4 Error Handling with Async/Await

Error handling with async/await is more intuitive than with Promise chains, as you can
use traditional try/catch blocks:

async function processData() {


try {
const rawData = await fetchData();
const processedData = await transformData(rawData);
return processedData;
} catch (error) {
console.error("Error processing data:", error);
// Handle the error or re-throw it
throw new Error(`Data processing failed: $
{error.message}`);
} finally {
// This will run regardless of success or failure
console.log("Data processing attempt completed");
}
}

14.3.5 Limitations and Best Practices

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

Unhandled Promise rejections in async functions will propagate to the caller:


async function riskyOperation() {
// No try/catch here
const data = await fetchData(); // This might reject
return processData(data);
}

// The error will propagate to here


riskyOperation()
.then(result => console.log(result))
.catch(error => console.error("Caught:", error));

Avoid Mixing Promises and Async/Await

For code clarity, it's generally best to stick with one style within a function:

// Not recommended: mixing styles


async function mixedStyle() {
const data = await fetchData();
return processData(data).then(result => {
return formatResult(result);
});
}

// Better: consistent async/await style


async function consistentStyle() {
const data = await fetchData();
const result = await processData(data);
return formatResult(result);
}

14.4 Real-World Examples


Let's explore some practical examples of using Promises and async/await in real-world
scenarios.

14.4.1 Loading Data with Fetch API

// 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();

return { user, posts };


}

// 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

async function fetchWithRetry(url, options = {}, maxRetries =


3) {
let retries = 0;

while (retries < maxRetries) {


try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
return await response.json();
} catch (error) {
retries++;
console.warn(`Attempt ${retries} failed. $
{maxRetries - retries} retries left.`);

if (retries >= maxRetries) {


throw new Error(`Max retries reached: $
{error.message}`);
}

// Wait before retrying (exponential backoff)


const delay = Math.min(1000 * Math.pow(2, retries),
10000);
await new Promise(resolve => setTimeout(resolve,
delay));
}
}
}

// Usage
fetchWithRetry('https://api.example.com/data')
.then(data => console.log("Data:", data))
.catch(error => console.error("Failed after retries:",
error));

14.4.3 Implementing a Timeout

function fetchWithTimeout(url, options = {}, timeout = 5000) {


return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timed
out')), timeout)
)
]);
}

// Usage
fetchWithTimeout('https://api.example.com/data', {}, 3000)
.then(response => response.json())
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));

14.4.4 Sequential vs. Concurrent Operations

// Sequential processing (one after another)


async function processItemsSequentially(items) {
const results = [];

for (const item of items) {


// Wait for each item to process before moving to the
next
const result = await processItem(item);
results.push(result);
}

return results;
}

// Concurrent processing (all at once)


async function processItemsConcurrently(items) {
// Create an array of promises
const promises = items.map(item => processItem(item));

// Wait for all promises to resolve


return Promise.all(promises);
}

// 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();

14.4.5 Building a Promise Queue

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;
}

const { promiseFactory, resolve, reject } =


this.queue.shift();
this.running++;

promiseFactory()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.next();
});
}

// Process all items in the queue


async processAll(items, processor) {
const results = [];

for (const item of items) {


const result = await this.add(() =>
processor(item));
results.push(result);
}

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];

async function processItem(item) {


console.log(`Started processing item ${item}`);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Finished processing item ${item}`);
return `Result ${item}`;
}

console.time('Queue Processing');
const results = await queue.processAll(items, processItem);
console.timeEnd('Queue Processing');

console.log("Results:", results);
}

testQueue();

14.5 Advanced Patterns

14.5.1 Cancellable Promises with AbortController

async function fetchWithCancellation(url) {


const controller = new AbortController();
const signal = controller.signal;

// Return both the promise and a cancel function


return {
promise: fetch(url, { signal })
.then(response => response.json()),
cancel: () => controller.abort()
};
}

// 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);
}
});

14.5.2 Promise Memoization

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();
});

// First call - will fetch from API


fetchUserMemoized(1)
.then(user => console.log("First call:", user.name));

// Second call with same argument - will use cached promise


setTimeout(() => {
fetchUserMemoized(1)
.then(user => console.log("Second call:", user.name));
}, 2000);

// Different argument - will fetch from API


setTimeout(() => {
fetchUserMemoized(2)
.then(user => console.log("Different user:",
user.name));
}, 3000);

14.5.3 Promise State Inspection

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;

const inspection = Promise.race([


promise
.then(value => {
state = "fulfilled";
result = value;
return value;
})
.catch(error => {
state = "rejected";
result = error;
throw error;
}),
Promise.resolve().then(() => ({ state, 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));

console.log(await inspectPromise(promise1)); // { state:


"fulfilled", result: "Success!" }
console.log(await inspectPromise(promise2)); // { state:
"rejected", result: "Failure!" }
console.log(await inspectPromise(promise3)); // { state:
"pending", result: undefined }

// Wait for the delayed promise to resolve


await new Promise(resolve => setTimeout(resolve, 1500));
console.log(await inspectPromise(promise3)); // { state:
"fulfilled", result: "Delayed" }
}

testInspection();

14.6 Chapter Summary and Exercises

Summary

In this chapter, we explored JavaScript's mechanisms for handling asynchronous


operations:

• 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

Understanding these concepts is crucial for writing efficient, maintainable asynchronous


code in JavaScript.

Exercises

Exercise 1: Promise Basics

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 2: Promise Combinators

1. Use Promise.all() to fetch data from multiple API endpoints simultaneously.


2. Implement a function that uses Promise.race() to fetch data with a timeout.
3. Use Promise.allSettled() to attempt multiple operations and report on all
results, regardless of success or failure.
4. Implement a function similar to Promise.any() that returns the first successful
result or an array of all errors if all promises fail.
5. Create a function that retries a Promise-based operation a specified number of
times before giving up.

Exercise 3: Async/Await

1. Convert a complex Promise chain to use async/await syntax.


2. Implement proper error handling with try/catch blocks in an async function.
3. Create a function that processes an array of items sequentially using async/await.
4. Create a function that processes an array of items concurrently (with a concurrency
limit) using async/await.
5. Implement a sleep function using async/await and use it in an async function.
Exercise 4: Real-World Application

Build a simple weather dashboard application that:

1. Fetches weather data from a public API (e.g., OpenWeatherMap)


2. Implements error handling for failed API requests
3. Shows a loading indicator while data is being fetched
4. Allows users to search for weather by city name
5. Caches results to avoid unnecessary API calls
6. Implements a retry mechanism for failed requests
7. Uses async/await for all asynchronous operations

Exercise 5: Advanced Challenge

Implement a Promise-based task scheduler with the following features:

1. Add tasks to a queue with different priority levels


2. Execute tasks concurrently with a configurable concurrency limit
3. Allow tasks to be cancelled before execution
4. Implement task timeouts
5. Provide progress updates for long-running tasks
6. Handle task dependencies (Task B can only start after Task A completes)
7. Implement error handling and retries for failed tasks

By completing these exercises, you'll gain practical experience with Promises and async/
await, which are essential for modern JavaScript development.

Chapter 15: Modern JavaScript APIs


Modern browsers provide a rich set of APIs that extend JavaScript's capabilities beyond
the core language features. These APIs enable web applications to interact with various
aspects of the browser and device, from storing data locally to accessing geolocation
information. This chapter explores several important modern JavaScript APIs that are
widely supported across browsers.

15.1 Web Storage API


The Web Storage API provides mechanisms for storing data in the browser, offering two
storage objects:

1. localStorage : Persists data with no expiration date


2. sessionStorage : Stores data for the duration of the page session

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"

// Removing specific data


localStorage.removeItem('username');

// Clearing all data


localStorage.clear();

// Checking if a key exists


if (localStorage.getItem('username') !== null) {
console.log('Username exists in localStorage');
}

// Getting the number of items


console.log(localStorage.length);

// Getting key by index


const key = localStorage.key(0);
console.log(key, localStorage.getItem(key));

15.1.2 sessionStorage

sessionStorage works similarly to localStorage , but data is cleared when the


page session ends (when the tab or browser is closed):
// Storing data
sessionStorage.setItem('currentPage', '5');
sessionStorage.setItem('searchResults', JSON.stringify([
{ id: 1, title: 'Result 1' },
{ id: 2, title: 'Result 2' }
]));

// 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"

// The API is identical to localStorage


sessionStorage.removeItem('currentPage');
sessionStorage.clear();

15.1.3 Storage Events

When storage changes in one tab, other tabs can listen for these changes:

// Add event listener for storage changes


window.addEventListener('storage', (event) => {
console.log('Storage changed in another tab/window');
console.log('Key:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('Storage area:', event.storageArea ===
localStorage ? 'localStorage' : 'sessionStorage');
console.log('URL of page that made the change:', event.url);
});

// Note: This event is not triggered in the same tab that made
the change

15.1.4 Storage Limitations

Web Storage has some limitations to be aware of:

• Storage capacity is typically limited to 5-10MB per domain


• Data is stored as strings (objects must be serialized/deserialized)
• Operations are synchronous (can block the main thread)
• No indexing or advanced query capabilities
• No built-in expiration mechanism
15.2 IndexedDB API
IndexedDB is a low-level API for client-side storage of significant amounts of structured
data, including files/blobs. It uses indexes to enable high-performance searches.

15.2.1 Basic IndexedDB Operations

// Open a database
const request = indexedDB.open('MyDatabase', 1);

// Handle database creation/upgrade


request.onupgradeneeded = (event) => {
const db = event.target.result;

// Create an object store (similar to a table)


const store = db.createObjectStore('customers', { keyPath:
'id', autoIncrement: true });

// Create indexes for searching


store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });

console.log('Database setup complete');


};

// Handle successful database open


request.onsuccess = (event) => {
const db = event.target.result;
console.log('Database opened successfully');

// Start a transaction and get the object store


const transaction = db.transaction(['customers'],
'readwrite');
const store = transaction.objectStore('customers');

// Add data
const customer = {
name: 'John Doe',
email: '[email protected]',
age: 35
};

const addRequest = store.add(customer);

addRequest.onsuccess = () => {
console.log('Customer added, ID:', addRequest.result);
};

// Handle transaction completion


transaction.oncomplete = () => {
console.log('Transaction completed');
db.close();
};
};

// Handle errors
request.onerror = (event) => {
console.error('Database error:', event.target.error);
};

15.2.2 Reading Data from IndexedDB

function getCustomerById(id) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);

request.onsuccess = (event) => {


const db = event.target.result;
const transaction = db.transaction(['customers'],
'readonly');
const store = transaction.objectStore('customers');

const getRequest = store.get(id);

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);
});

15.2.3 Using Cursors for Iteration

function getAllCustomers() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);

request.onsuccess = (event) => {


const db = event.target.result;
const transaction = db.transaction(['customers'],
'readonly');
const store = transaction.objectStore('customers');

const customers = [];

// Open a cursor to iterate through all records


const cursorRequest = store.openCursor();

cursorRequest.onsuccess = (event) => {


const cursor = event.target.result;

if (cursor) {
// Add the customer to our array
customers.push(cursor.value);

// Move to the next record


cursor.continue();
} else {
// No more records, resolve with the array
resolve(customers);
}
};

cursorRequest.onerror = () => {
reject(cursorRequest.error);
};

transaction.oncomplete = () => {
db.close();
};
};

request.onerror = () => {
reject(request.error);
};
});
}

15.2.4 Using Indexes for Searching

function findCustomersByName(name) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);

request.onsuccess = (event) => {


const db = event.target.result;
const transaction = db.transaction(['customers'],
'readonly');
const store = transaction.objectStore('customers');

// Get the index we want to use


const index = store.index('name');

// Use only exact matches


const keyRange = IDBKeyRange.only(name);

// Open a cursor on the index with the key range


const cursorRequest = index.openCursor(keyRange);

const customers = [];

cursorRequest.onsuccess = (event) => {


const cursor = event.target.result;

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);
};
});
}

15.2.5 Updating and Deleting Records

// Update a customer
function updateCustomer(id, updates) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);

request.onsuccess = (event) => {


const db = event.target.result;
const transaction = db.transaction(['customers'],
'readwrite');
const store = transaction.objectStore('customers');

// First get the current record


const getRequest = store.get(id);

getRequest.onsuccess = () => {
if (!getRequest.result) {
reject(new Error('Customer not found'));
return;
}

// Merge the current record with updates


const updatedCustomer =
{ ...getRequest.result, ...updates };

// Put the updated record back


const putRequest = store.put(updatedCustomer);

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);

request.onsuccess = (event) => {


const db = event.target.result;
const transaction = db.transaction(['customers'],
'readwrite');
const store = transaction.objectStore('customers');

const deleteRequest = store.delete(id);

deleteRequest.onsuccess = () => {
resolve(true);
};

deleteRequest.onerror = () => {
reject(deleteRequest.error);
};

transaction.oncomplete = () => {
db.close();
};
};

request.onerror = () => {
reject(request.error);
};
});
}

15.3 Geolocation API


The Geolocation API allows web applications to access the user's geographical location
information.

15.3.1 Getting the Current Position

// Check if geolocation is supported


if ('geolocation' in navigator) {
// Get current position
navigator.geolocation.getCurrentPosition(
// Success callback
(position) => {
console.log('Latitude:', position.coords.latitude);
console.log('Longitude:',
position.coords.longitude);
console.log('Accuracy:', position.coords.accuracy,
'meters');

// Additional information (may not be available on


all devices)
if (position.coords.altitude !== null) {
console.log('Altitude:',
position.coords.altitude, 'meters');
console.log('Altitude Accuracy:',
position.coords.altitudeAccuracy, 'meters');
}

if (position.coords.speed !== null) {


console.log('Speed:', position.coords.speed,
'meters/second');
}

if (position.coords.heading !== null) {


console.log('Heading:',
position.coords.heading, 'degrees');
}

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');
}

15.3.2 Watching Position Changes

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);

// Update map or UI with new position


updateMap(position.coords);
},
// Error callback
(error) => {
console.error('Error watching position:',
error.message);
},
// Options
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);

console.log('Started watching position. Watch ID:',


watchId);
} else {
console.error('Geolocation is not supported by this
browser');
}
}

function stopWatchingPosition() {
if (watchId !== undefined) {
navigator.geolocation.clearWatch(watchId);
console.log('Stopped watching position');
watchId = undefined;
}
}

// Example function to update a map (implementation would depend


on your mapping library)
function updateMap(coords) {
console.log('Updating map with coordinates:', coords);
// Implementation would go here
}

15.3.3 Calculating Distance Between Coordinates

// Calculate distance between two points using the Haversine


formula
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3; // Earth's radius in meters

const φ1 = lat1 * Math.PI / 180; // Convert to radians


const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lon2 - lon1) * Math.PI / 180;

const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +


Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);

const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

const distance = R * c; // Distance in meters

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.

15.4.1 Creating a Basic Web Worker

Main script (main.js):

// Check if Web Workers are supported


if (window.Worker) {
// Create a new worker
const worker = new Worker('worker.js');

// Send a message to the worker


worker.postMessage({
command: 'calculate',
data: { numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
});

// Listen for messages from the worker


worker.onmessage = (event) => {
console.log('Result received from worker:', event.data);
};

// Handle errors
worker.onerror = (error) => {
console.error('Worker error:', error.message);
console.error('Error filename:', error.filename);
console.error('Error line number:', error.lineno);
};

// Terminate the worker when done


function terminateWorker() {
worker.terminate();
console.log('Worker terminated');
}
} else {
console.error('Web Workers are not supported in this
browser');
}

Worker script (worker.js):

// Listen for messages from the main script


self.onmessage = (event) => {
console.log('Message received in worker:', event.data);
const { command, data } = event.data;

if (command === 'calculate') {


// Perform a CPU-intensive calculation
const result = performCalculation(data.numbers);

// Send the result back to the main script


self.postMessage({
command: 'result',
data: result
});
}
};

// Example calculation function


function performCalculation(numbers) {
console.log('Performing calculation on:', numbers);

// Simulate a CPU-intensive task


let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i % 10;
}

// Calculate the sum of the input numbers


const inputSum = numbers.reduce((acc, num) => acc + num, 0);

return {
inputSum,
processingTime: new Date().getTime()
};
}

// Handle errors
self.onerror = (error) => {
console.error('Error in worker:', error);
};

15.4.2 Transferring Data with Transferable Objects

For large data like ArrayBuffers, you can transfer ownership instead of copying:

// In the main script


const buffer = new ArrayBuffer(1024 * 1024 * 32); // 32MB buffer
const view = new Uint8Array(buffer);

// Fill the buffer with data


for (let i = 0; i < view.length; i++) {
view[i] = i % 256;
}
// Transfer the buffer to the worker (ownership is transferred,
not copied)
worker.postMessage({ buffer }, [buffer]);

// After transfer, the buffer is no longer usable in the main


thread
console.log('Buffer byteLength after transfer:',
buffer.byteLength); // 0

// In the worker script


self.onmessage = (event) => {
const { buffer } = event.data;

// Use the transferred buffer


const view = new Uint8Array(buffer);
console.log('Received buffer size:', buffer.byteLength);
console.log('First few bytes:', view.slice(0, 10));

// Process the data...

// Transfer it back when done


self.postMessage({ buffer }, [buffer]);
};

15.4.3 Shared Workers

Shared Workers can be accessed by multiple scripts or windows from the same origin:

Main script:

// Create a shared worker


const sharedWorker = new SharedWorker('shared-worker.js');

// Connect to the shared worker


sharedWorker.port.start();

// Send a message
sharedWorker.port.postMessage({
source: 'Window ' + window.name,
message: 'Hello from the main script'
});

// Listen for messages


sharedWorker.port.onmessage = (event) => {
console.log('Message from shared worker:', event.data);
};
Shared worker script (shared-worker.js):

// Keep track of connected ports


const ports = [];

// Listen for connections


self.onconnect = (event) => {
const port = event.ports[0];
ports.push(port);

// Start the port


port.start();

// Listen for messages on this port


port.onmessage = (event) => {
console.log('Shared worker received:', event.data);

// Broadcast to all connected ports


ports.forEach(p => {
p.postMessage({
source: 'Shared Worker',
originalMessage: event.data,
connectionCount: ports.length
});
});
};
};

15.5 Intersection Observer API


The Intersection Observer API provides a way to asynchronously observe changes in the
intersection of a target element with an ancestor element or the viewport.

15.5.1 Basic Usage

// Callback function to execute when observed elements intersect


function handleIntersection(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is now visible:',
entry.target);

// Do something with the visible element


entry.target.classList.add('visible');

// Optionally stop observing this element


// observer.unobserve(entry.target);
} else {
console.log('Element is no longer visible:',
entry.target);
entry.target.classList.remove('visible');
}
});
}

// Create an observer with options


const observer = new IntersectionObserver(handleIntersection, {
root: null, // Use the viewport as the root
rootMargin: '0px', // No margin around the root
threshold: 0.1 // Trigger when 10% of the element is
visible
});

// Start observing elements


document.querySelectorAll('.observe-me').forEach(element => {
observer.observe(element);
});

// Later, to stop observing


function stopObserving() {
observer.disconnect();
}

15.5.2 Lazy Loading Images

// Create an observer for lazy loading images


const imageObserver = new IntersectionObserver((entries,
observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;

// Replace the src attribute with the data-src value


img.src = img.dataset.src;

// Remove the placeholder class


img.classList.remove('placeholder');

// Stop observing this image


observer.unobserve(img);
}
});
}, {
rootMargin: '50px 0px', // Start loading images when
they're 50px from entering the viewport
threshold: 0.01 // Trigger when just 1% of the
image is visible
});

// Observe all images with data-src attribute


document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});

15.5.3 Infinite Scrolling

// Create an observer for infinite scrolling


const loadMoreObserver = new IntersectionObserver((entries,
observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Load more content
loadMoreContent()
.then(hasMoreContent => {
if (!hasMoreContent) {
// No more content to load, disconnect
the observer
observer.disconnect();
}
})
.catch(error => {
console.error('Error loading more
content:', error);
});
}
});
}, {
rootMargin: '200px 0px', // Start loading when 200px from
the bottom
threshold: 0.1
});

// Observe the loading trigger element


const loadingTrigger = document.getElementById('loading-
trigger');
loadMoreObserver.observe(loadingTrigger);

// Example function to load more content


async function loadMoreContent() {
console.log('Loading more content...');

// Simulate an API call


return new Promise((resolve) => {
setTimeout(() => {
// Add new content to the page
const contentContainer =
document.getElementById('content-container');
for (let i = 0; i < 10; i++) {
const item = document.createElement('div');
item.classList.add('content-item');
item.textContent = `Item $
{Math.floor(Math.random() * 1000)}`;
contentContainer.appendChild(item);
}

// Simulate whether there's more content to load


const hasMoreContent = Math.random() > 0.2; // 80%
chance of more content

console.log('Content loaded. More content


available:', hasMoreContent);
resolve(hasMoreContent);
}, 1000);
});
}

15.6 Web Notifications API


The Web Notifications API allows web applications to display system notifications to the
user.

15.6.1 Requesting Permission and Displaying Notifications

// Check if the browser supports notifications


if ('Notification' in window) {
// Request permission
async function requestNotificationPermission() {
try {
const permission = await
Notification.requestPermission();
console.log('Notification permission:', permission);
return permission;
} catch (error) {
console.error('Error requesting notification
permission:', error);
return 'denied';
}
}

// 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
});

// Handle notification events


notification.onclick = (event) => {
console.log('Notification clicked:', event);
window.focus();
notification.close();
};

notification.onclose = (event) => {


console.log('Notification closed:', event);
};

notification.onerror = (event) => {


console.error('Notification error:', event);
};

notification.onshow = (event) => {


console.log('Notification shown:', event);
};

return notification;
} else {
console.warn('Notification permission not granted');
return null;
}
}

// Example usage
document.getElementById('request-
permission').addEventListener('click', async () => {
const permission = await
requestNotificationPermission();

if (permission === 'granted') {


showNotification('Hello from JavaScript!', {
body: 'This is a notification from your web
app.',
icon: '/images/notification-icon.png',
requireInteraction: true
});
}
});
} else {
console.error('This browser does not support
notifications');
}

15.6.2 Using Service Workers with Notifications

Service Workers can display notifications even when the page is closed:

Main script:

// Register the service worker


if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with
scope:', registration.scope);
})
.catch(error => {
console.error('Service Worker registration
failed:', error);
});
}

// Send a notification via the service worker


function sendNotificationViaServiceWorker(title, options) {
if ('serviceWorker' in navigator && Notification.permission
=== 'granted') {
navigator.serviceWorker.ready.then(registration => {
registration.showNotification(title, options);
});
}
}

// 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
}
});
});

Service Worker (service-worker.js):

// Listen for notification clicks


self.addEventListener('notificationclick', event => {
console.log('Notification clicked:',
event.notification.tag);

// Close the notification


event.notification.close();

// Handle notification actions


if (event.action === 'explore') {
console.log('User clicked "Explore" action');

// Open a URL from the notification data


const url = event.notification.data.url || '/';

// Open or focus on a client window


event.waitUntil(
clients.matchAll({ type:
'window' }).then(clientList => {
// Check if a window is already open
for (const client of clientList) {
if (client.url === url && 'focus' in
client) {
return client.focus();
}
}

// If no window is open, open a new one


if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
} else if (event.action === 'close') {
console.log('User clicked "Close" action');
// No additional action needed
}
});

// Listen for notification close events


self.addEventListener('notificationclose', event => {
console.log('Notification closed:', event.notification.tag);
});
15.7 Web Speech API
The Web Speech API provides speech recognition and speech synthesis capabilities.

15.7.1 Speech Synthesis (Text-to-Speech)

// Check if speech synthesis is supported


if ('speechSynthesis' in window) {
// Get the speech synthesis object
const synth = window.speechSynthesis;

// Function to speak text


function speak(text, options = {}) {
// Create a new utterance
const utterance = new SpeechSynthesisUtterance(text);

// Set options
utterance.lang = options.lang || 'en-US';
utterance.pitch = options.pitch || 1;
utterance.rate = options.rate || 1;
utterance.volume = options.volume || 1;

// Set voice if specified


if (options.voice) {
utterance.voice = options.voice;
}

// Event handlers
utterance.onstart = () => {
console.log('Speech started');
};

utterance.onend = () => {
console.log('Speech ended');
};

utterance.onerror = (event) => {


console.error('Speech error:', event.error);
};

utterance.onpause = () => {
console.log('Speech paused');
};

utterance.onresume = () => {
console.log('Speech resumed');
};

utterance.onboundary = (event) => {


console.log('Speech boundary reached:', event.name,
event.charIndex);
};

// Speak the utterance


synth.speak(utterance);

return utterance;
}

// Get available voices


function getVoices() {
return new Promise((resolve) => {
let voices = synth.getVoices();

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();

console.log('Available voices:', voices);

// Create a voice selector


const voiceSelect = document.getElementById('voice-
select');

voices.forEach((voice, index) => {


const option = document.createElement('option');
option.value = index;
option.textContent = `${voice.name} ($
{voice.lang})`;
voiceSelect.appendChild(option);
});

// Set up the speak button


document.getElementById('speak-
button').addEventListener('click', () => {
const text = document.getElementById('text-to-
speak').value;
const selectedVoice = voices[voiceSelect.value];

speak(text, {
voice: selectedVoice,
rate: document.getElementById('rate-
slider').value,
pitch: document.getElementById('pitch-
slider').value
});
});

// Set up control buttons


document.getElementById('pause-
button').addEventListener('click', () => {
if (synth.speaking) {
synth.pause();
}
});

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');
}

15.7.2 Speech Recognition

// Check if speech recognition is supported


const SpeechRecognition = window.SpeechRecognition ||
window.webkitSpeechRecognition;

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 = '';

for (let i = event.resultIndex; i <


event.results.length; i++) {
const transcript = event.results[i][0].transcript;

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);
};

// Handle end of recognition


recognition.onend = () => {
console.log('Speech recognition ended');
};

// 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');
}

15.8 Chapter Summary and Exercises

Summary

In this chapter, we explored several modern JavaScript APIs:

• Web Storage API: localStorage and sessionStorage for storing key-value


pairs in the browser
• IndexedDB API: A powerful client-side database for storing structured data
• Geolocation API: Accessing the user's geographical location
• Web Workers API: Running JavaScript in background threads
• Intersection Observer API: Detecting when elements enter or exit the viewport
• Web Notifications API: Displaying system notifications to users
• Web Speech API: Converting text to speech and recognizing speech input

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 1: Web Storage

1. Create a simple note-taking application that saves notes to localStorage .


2. Implement a theme switcher that remembers the user's preference using
localStorage .
3. Build a shopping cart that persists items between page reloads using
sessionStorage .
4. Create a function that checks the available storage space and warns users when it's
nearly full.
5. Implement a storage manager that can export and import data from
localStorage .

Exercise 2: IndexedDB

1. Create a contact management application that stores contacts in IndexedDB.


2. Implement search functionality using indexes to find contacts by name or email.
3. Add the ability to store profile images as Blobs in the database.
4. Create a versioning system that upgrades the database schema when needed.
5. Implement a backup and restore feature for the database.

Exercise 3: Geolocation and Maps

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.

Exercise 4: Web Workers

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.

Exercise 5: Advanced Project

Build a progressive web application (PWA) that combines multiple modern APIs:

1. Use IndexedDB to store application data locally.


2. Implement offline functionality using Service Workers.
3. Use the Geolocation API to track and display the user's location.
4. Implement push notifications using the Web Notifications API and Service Workers.
5. Use Web Workers for computationally intensive tasks.
6. Implement the Intersection Observer API for lazy loading images and infinite
scrolling.
7. Add speech recognition for voice commands and speech synthesis for feedback.

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.

16.1 Understanding JavaScript Performance


Before diving into optimization techniques, it's important to understand how JavaScript
performance works and how to measure it.

16.1.1 The JavaScript Engine

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)

16.1.2 Performance Bottlenecks

Common performance bottlenecks in JavaScript applications include:

1. CPU-intensive operations: Complex calculations, large loops, recursive functions


2. Memory usage: Memory leaks, excessive object creation
3. DOM manipulation: Frequent updates, complex layouts, reflows
4. Network requests: Slow API calls, large payloads, excessive requests
5. Rendering: Complex animations, inefficient CSS

16.1.3 Measuring Performance

Before optimizing, you need to measure performance to identify bottlenecks:


Using the Performance API

// Measure execution time


performance.mark('startOperation');

// Code to measure
for (let i = 0; i < 1000000; i++) {
// Some operation
}

performance.mark('endOperation');
performance.measure('operationDuration', 'startOperation',
'endOperation');

const measurements = performance.getEntriesByType('measure');


console.log(`Operation took $
{measurements[0].duration.toFixed(2)} milliseconds`);

// Clear marks and measures


performance.clearMarks();
performance.clearMeasures();

Using console.time()

console.time('operationTimer');

// Code to measure
for (let i = 0; i < 1000000; i++) {
// Some operation
}

console.timeEnd('operationTimer'); // Outputs: "operationTimer:


123.45ms"

Using Chrome DevTools

Chrome DevTools provides powerful performance analysis tools:

1. Performance panel: Record and analyze runtime performance


2. Memory panel: Identify memory leaks and analyze memory usage
3. Network panel: Analyze network requests and responses
4. Lighthouse: Audit web app performance, accessibility, and more

16.2 Code Optimization Techniques


Let's explore various techniques to optimize JavaScript code.
16.2.1 Optimizing Loops

Loops are often a source of performance issues, especially when dealing with large
datasets.

Use appropriate loop types

const arr = new Array(1000000).fill(1);

// Traditional for loop (often fastest for arrays)


console.time('for');
for (let i = 0; i < arr.length; i++) {
// Operation
}
console.timeEnd('for');

// Cached length (slightly faster)


console.time('for-cached');
for (let i = 0, len = arr.length; i < len; i++) {
// Operation
}
console.timeEnd('for-cached');

// forEach (cleaner but slightly slower)


console.time('forEach');
arr.forEach(item => {
// Operation
});
console.timeEnd('forEach');

// for...of (good for iterables, slightly slower)


console.time('for-of');
for (const item of arr) {
// Operation
}
console.timeEnd('for-of');

// for...in (avoid for arrays, much slower)


console.time('for-in');
for (const i in arr) {
// Operation
}
console.timeEnd('for-in');

Minimize work inside loops

const items = new Array(10000).fill({ value: 5 });

// Bad: Accessing length property in each iteration


console.time('bad');
for (let i = 0; i < items.length; i++) {
const value = items[i].value;
// Operation with value
}
console.timeEnd('bad');

// Good: Caching length and minimizing property access


console.time('good');
const len = items.length;
for (let i = 0; i < len; i++) {
const item = items[i];
const value = item.value;
// Operation with value
}
console.timeEnd('good');

Use appropriate array methods

const numbers = new Array(10000).fill(0).map((_, i) => i);

// Using filter + map (traverses array twice)


console.time('filter-map');
const resultA = numbers
.filter(num => num % 2 === 0)
.map(num => num * 2);
console.timeEnd('filter-map');

// Using reduce (traverses array once)


console.time('reduce');
const resultB = numbers.reduce((acc, num) => {
if (num % 2 === 0) {
acc.push(num * 2);
}
return acc;
}, []);
console.timeEnd('reduce');

16.2.2 Optimizing Functions

Functions are fundamental to JavaScript, and optimizing them can significantly improve
performance.

Avoid unnecessary function calls

// Bad: Creating a function in a loop


console.time('function-in-loop');
for (let i = 0; i < 100000; i++) {
const double = function(x) {
return x * 2;
};
double(i);
}
console.timeEnd('function-in-loop');

// Good: Define function outside the loop


console.time('function-outside-loop');
function double(x) {
return x * 2;
}
for (let i = 0; i < 100000; i++) {
double(i);
}
console.timeEnd('function-outside-loop');

Use memoization for expensive calculations

// Without memoization
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

// With memoization
function memoizedFibonacci() {
const cache = {};

return function fib(n) {


if (n in cache) {
return cache[n];
}

if (n <= 1) {
return n;
}

const result = fib(n - 1) + fib(n - 2);


cache[n] = result;
return result;
};
}

const fibMemo = memoizedFibonacci();

// Compare performance
console.time('fibonacci-no-memo');
fibonacci(30);
console.timeEnd('fibonacci-no-memo');
console.time('fibonacci-memo');
fibMemo(30);
console.timeEnd('fibonacci-memo');

Use function inlining for small, frequently called functions

// Function call overhead can be significant for very small


functions
// called in tight loops

// With function call


console.time('with-function');
function add(a, b) {
return a + b;
}

let sum1 = 0;
for (let i = 0; i < 10000000; i++) {
sum1 = add(sum1, i);
}
console.timeEnd('with-function');

// With inlined code


console.time('inlined');
let sum2 = 0;
for (let i = 0; i < 10000000; i++) {
sum2 = sum2 + i; // Inlined
}
console.timeEnd('inlined');

16.2.3 Data Structure Optimization

Choosing the right data structure can dramatically impact performance.

Arrays vs. Objects

// Array lookup by index (fast)


console.time('array-lookup');
const arr = new Array(1000000).fill(42);
let value = arr[500000];
console.timeEnd('array-lookup');

// Object lookup by key (also fast)


console.time('object-lookup');
const obj = {};
for (let i = 0; i < 1000000; i++) {
obj[i] = 42;
}
value = obj[500000];
console.timeEnd('object-lookup');

// Array search (slow for large arrays)


console.time('array-search');
const index = arr.indexOf(42);
console.timeEnd('array-search');

// Object check (fast)


console.time('object-check');
const exists = 500000 in obj;
console.timeEnd('object-check');

Using Sets for unique values

// Array with duplicates


const arrayWithDuplicates = [];
for (let i = 0; i < 100000; i++) {
arrayWithDuplicates.push(i % 1000);
}

// Remove duplicates with filter (slow)


console.time('array-filter');
const uniqueArray = arrayWithDuplicates.filter(
(value, index, self) => self.indexOf(value) === index
);
console.timeEnd('array-filter');

// Remove duplicates with Set (fast)


console.time('set');
const uniqueSet = [...new Set(arrayWithDuplicates)];
console.timeEnd('set');

// Checking if a value exists


console.time('array-includes');
const hasValue = arrayWithDuplicates.includes(500);
console.timeEnd('array-includes');

console.time('set-has');
const set = new Set(arrayWithDuplicates);
const hasValueInSet = set.has(500);
console.timeEnd('set-has');

Using Maps for complex keys

// Object as a map (only string keys)


console.time('object-as-map');
const userRoles = {};
for (let i = 0; i < 10000; i++) {
const userId = `user${i}`;
userRoles[userId] = 'user';
}
const role = userRoles['user5000'];
console.timeEnd('object-as-map');

// Map (any type as keys)


console.time('map');
const userRolesMap = new Map();
for (let i = 0; i < 10000; i++) {
const userId = `user${i}`;
userRolesMap.set(userId, 'user');
}
const roleFromMap = userRolesMap.get('user5000');
console.timeEnd('map');

// Using objects as keys (not possible with regular objects)


const userObjects = [];
for (let i = 0; i < 100; i++) {
userObjects.push({ id: i, name: `User ${i}` });
}

const userDataMap = new Map();


for (const user of userObjects) {
userDataMap.set(user, { lastLogin: new Date() });
}

const userData = userDataMap.get(userObjects[50]);

16.2.4 String Optimization

String operations can be expensive, especially when dealing with large strings or
frequent concatenations.

String concatenation

// Using + operator (creates new strings)


console.time('plus-operator');
let result1 = '';
for (let i = 0; i < 100000; i++) {
result1 += i;
}
console.timeEnd('plus-operator');

// Using array join (more efficient for many concatenations)


console.time('array-join');
const parts = [];
for (let i = 0; i < 100000; i++) {
parts.push(i);
}
const result2 = parts.join('');
console.timeEnd('array-join');

// Using template literals (convenient but similar to +


operator)
console.time('template-literals');
let result3 = '';
for (let i = 0; i < 100000; i++) {
result3 += `${i}`;
}
console.timeEnd('template-literals');

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');

// Reusing regex objects


console.time('regex-reuse');
const wordPattern = /\b\w+\b/g;
const words = text.match(wordPattern);
console.timeEnd('regex-reuse');

// Creating regex in loops (avoid)


console.time('regex-in-loop');
for (let i = 0; i < 1000; i++) {
const pattern = new RegExp(`\\b\\w{${i % 10}}\\b`, 'g');
text.match(pattern);
}
console.timeEnd('regex-in-loop');
16.3 DOM Optimization
DOM operations are often the most expensive part of web applications. Optimizing them
can significantly improve perceived performance.

16.3.1 Minimizing DOM Access

// Bad: Accessing the DOM in each iteration


console.time('dom-in-loop');
const list = document.getElementById('myList');
for (let i = 0; i < 1000; i++) {
list.innerHTML += `<li>Item ${i}</li>`;
}
console.timeEnd('dom-in-loop');

// Good: Batch DOM updates


console.time('dom-batch');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment);
console.timeEnd('dom-batch');

16.3.2 Avoiding Layout Thrashing

Layout thrashing occurs when you repeatedly read and write to the DOM, forcing the
browser to recalculate layouts.

// Bad: Interleaving reads and writes


console.time('layout-thrashing');
const boxes = document.querySelectorAll('.box');
for (let i = 0; i < boxes.length; i++) {
const width = boxes[i].offsetWidth; // Read
boxes[i].style.width = (width * 2) + 'px'; // Write
}
console.timeEnd('layout-thrashing');

// Good: Batch reads, then writes


console.time('no-thrashing');
const boxesArray =
Array.from(document.querySelectorAll('.box'));
// Read phase
const widths = boxesArray.map(box => box.offsetWidth);
// Write phase
boxesArray.forEach((box, i) => {
box.style.width = (widths[i] * 2) + 'px';
});
console.timeEnd('no-thrashing');

16.3.3 Using DocumentFragment

function createList(items) {
const fragment = document.createDocumentFragment();

items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});

return fragment;
}

const items = Array.from({ length: 1000 }, (_, i) => `Item ${i}


`);
const list = document.getElementById('myList');
list.appendChild(createList(items));

16.3.4 Event Delegation

// Bad: Attaching event listeners to many elements


console.time('many-listeners');
document.querySelectorAll('.button').forEach(button => {
button.addEventListener('click', function() {
console.log('Button clicked:', this.textContent);
});
});
console.timeEnd('many-listeners');

// Good: Using event delegation


console.time('event-delegation');
document.getElementById('buttonContainer').addEventListener('click',
function(event) {
if (event.target.classList.contains('button')) {
console.log('Button clicked:',
event.target.textContent);
}
});
console.timeEnd('event-delegation');
16.3.5 Optimizing Animations

// Bad: Changing multiple CSS properties individually


function animateBadly(element) {
element.style.opacity = '0.5';
element.style.transform = 'translateX(100px)';
element.style.backgroundColor = 'red';
}

// Good: Using CSS classes


function animateWell(element) {
element.classList.add('animated');
}
// CSS: .animated { opacity: 0.5; transform: translateX(100px);
background-color: red; }

// Better: Using requestAnimationFrame


function animateSmooth(element, startPos, endPos, duration) {
const startTime = performance.now();

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);
}

// Best: Using CSS transitions or Web Animations API


element.style.transition = 'transform 0.5s ease';
element.style.transform = 'translateX(100px)';

// Or with Web Animations API


element.animate([
{ transform: 'translateX(0)' },
{ transform: 'translateX(100px)' }
], {
duration: 500,
easing: 'ease',
fill: 'forwards'
});
16.4 Memory Management
Proper memory management is crucial for maintaining application performance over
time.

16.4.1 Understanding Garbage Collection

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).

// Objects are created and automatically garbage collected when


no longer needed
function createObjects() {
const obj1 = { name: 'Object 1' }; // Created
const obj2 = { name: 'Object 2' }; // Created

return obj1; // obj1 is returned, obj2 becomes eligible for


garbage collection
}

const result = createObjects();


// obj1 is still referenced by 'result'
// obj2 is garbage collected

16.4.2 Common Memory Leaks

Forgotten event listeners

// Memory leak: Event listener keeps reference to element and


handler
function addLeakyHandler() {
const button = document.getElementById('myButton');
const data = new Array(10000).fill('some data'); // Large
data

button.addEventListener('click', function() {
console.log('Button clicked', data.length);
});
}

// Fixed: Remove event listener when no longer needed


function addCleanHandler() {
const button = document.getElementById('myButton');
const data = new Array(10000).fill('some data');
const handler = function() {
console.log('Button clicked', data.length);
};

button.addEventListener('click', handler);

// Return a cleanup function


return function cleanup() {
button.removeEventListener('click', handler);
};
}

// Usage
const cleanup = addCleanHandler();
// Later, when the handler is no longer needed
cleanup();

Closures capturing large objects

// Memory leak: Closure captures large data


function createLeakyClosure() {
const largeData = new Array(1000000).fill('data');

return function() {
console.log('Length:', largeData.length);
};
}

// Fixed: Only capture what's needed


function createEfficientClosure() {
const largeData = new Array(1000000).fill('data');
const length = largeData.length;

return function() {
console.log('Length:', length);
};
}

Detached DOM elements

// Memory leak: Keeping reference to removed DOM elements


let detachedElements = [];

function createDetachedElements() {
const div = document.createElement('div');
div.innerHTML = 'This is a detached element';

// Store reference to the element, but never add it to the


DOM
detachedElements.push(div);
}

// Fixed: Clear references when no longer needed


function cleanup() {
detachedElements = [];
}

16.4.3 Detecting Memory Leaks

You can use Chrome DevTools to detect memory leaks:

1. Open the Memory panel


2. Take a heap snapshot
3. Perform actions that might cause leaks
4. Take another snapshot
5. Compare snapshots to identify retained objects

16.4.4 Best Practices for Memory Management

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

// Using WeakMap to avoid memory leaks


const cache = new WeakMap();

function processUser(user) {
if (cache.has(user)) {
return cache.get(user);
}

const result = expensiveOperation(user);


cache.set(user, result);
return result;
}

// When the user object is no longer referenced elsewhere,


// it can be garbage collected along with its entry in the
WeakMap

16.5 Network Optimization


Network performance is often the biggest bottleneck in web applications.

16.5.1 Reducing Request Size

// Compress data before sending


function sendCompressedData(data) {
// Convert object to JSON string
const jsonString = JSON.stringify(data);

// In a real app, you might use a compression library like


pako
// const compressed = pako.deflate(jsonString);

// For demonstration, we'll just track the size


console.log('Original size:', jsonString.length, 'bytes');

// Send the data


fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 'Content-Encoding': 'deflate'
},
body: jsonString
});
}

16.5.2 Caching API Responses

// Simple in-memory cache


const apiCache = new Map();

async function fetchWithCache(url, options = {}) {


const cacheKey = `${url}-${JSON.stringify(options)}`;

// Check if we have a cached response


if (apiCache.has(cacheKey)) {
const cachedData = apiCache.get(cacheKey);

// Check if the cache is still valid


if (cachedData.expiry > Date.now()) {
console.log('Cache hit:', url);
return cachedData.data;
} else {
console.log('Cache expired:', url);
apiCache.delete(cacheKey);
}
}

// If not cached or expired, fetch from API


console.log('Cache miss:', url);
const response = await fetch(url, options);

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}
`);
}

const data = await response.json();

// Cache the response for 5 minutes


apiCache.set(cacheKey, {
data,
expiry: Date.now() + 5 * 60 * 1000
});

return data;
}

16.5.3 Batching API Requests

// Request queue for batching


class RequestBatcher {
constructor(batchUrl, maxBatchSize = 10, maxWaitTime = 100)
{
this.batchUrl = batchUrl;
this.maxBatchSize = maxBatchSize;
this.maxWaitTime = maxWaitTime;
this.queue = [];
this.timeout = null;
}

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;
}

// Take items from the queue


const batch = this.queue.splice(0, this.maxBatchSize);
const requests = batch.map(item => item.request);

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}`);
}

const results = await response.json();

// Resolve individual promises with their results


batch.forEach((item, index) => {
item.resolve(results[index]);
});
} catch (error) {
// Reject all promises in the batch
batch.forEach(item => {
item.reject(error);
});
}

// Process any remaining items


if (this.queue.length > 0) {
this.scheduleProcessing();
}
}
}

// Usage
const batcher = new RequestBatcher('/api/batch');

async function fetchUserData(userId) {


return batcher.add({ type: 'user', id: userId });
}

async function fetchProductData(productId) {


return batcher.add({ type: 'product', id: productId });
}

// These will be batched together if called within maxWaitTime


fetchUserData(1).then(data => console.log('User data:', data));
fetchProductData(42).then(data => console.log('Product data:',
data));

16.6 Rendering Optimization


Optimizing rendering performance is crucial for smooth user experiences, especially on
mobile devices.

16.6.1 Minimizing Reflows and Repaints

// Bad: Causes multiple reflows


function updateElementsBadly(elements) {
elements.forEach(element => {
element.style.width = '100px';
console.log(element.offsetHeight); // Forces reflow
element.style.height = '100px';
console.log(element.offsetWidth); // Forces reflow
element.style.margin = '10px';
});
}

// Good: Batch style changes


function updateElementsWell(elements) {
// Read phase
const dimensions = elements.map(element => ({
height: element.offsetHeight,
width: element.offsetWidth
}));

// Write phase
elements.forEach((element, i) => {
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
});

// Now we can use the dimensions if needed


console.log('Dimensions:', dimensions);
}

16.6.2 Using CSS Transforms

// Bad: Changes layout properties


function animatePositionBadly(element) {
let position = 0;

setInterval(() => {
position += 5;
element.style.left = position + 'px'; // Triggers layout
}, 16);
}

// Good: Uses transforms


function animatePositionWell(element) {
let position = 0;

function animate() {
position += 5;
element.style.transform = `translateX(${position}
px)`; // No layout changes
requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
}

16.6.3 Using will-change

// Hint to the browser that an element will change


function prepareForAnimation(element) {
// Add will-change before animation starts
element.style.willChange = 'transform, opacity';

// Start animation after a short delay


setTimeout(() => {
element.classList.add('animate');

// Remove will-change after animation completes


element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
}, { once: true });
}, 100);
}

16.6.4 Debouncing and Throttling

// Debounce: Execute function only after a certain time has


passed since last call
function debounce(func, delay) {
let timeoutId;

return function(...args) {
clearTimeout(timeoutId);

timeoutId = setTimeout(() => {


func.apply(this, args);
}, delay);
};
}

// Throttle: Execute function at most once per specified time


period
function throttle(func, limit) {
let inThrottle;

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);

const throttledScroll = throttle(() => {


console.log('Scroll event throttled');
// Expensive operation
}, 100);

window.addEventListener('resize', debouncedResize);
window.addEventListener('scroll', throttledScroll);

16.7 Advanced Optimization Techniques

16.7.1 Web Workers for CPU-Intensive Tasks

// 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);
};

// Send data to the worker


worker.postMessage({
action: 'calculate',
data: Array.from({ length: 10000000 }, (_, i) => i)
});
}

// worker.js
self.onmessage = function(event) {
const { action, data } = event.data;

if (action === 'calculate') {


// Perform CPU-intensive calculation
const result = data.reduce((sum, num) => sum + num, 0);

// Send result back to main thread


self.postMessage(result);
}
};
16.7.2 Virtual DOM and DOM Recycling

// Simple virtual DOM implementation


class VirtualDOM {
constructor() {
this.virtualElements = new Map();
this.realElements = new Map();
}

createElement(type, props, ...children) {


return { type, props: props || {}, children };
}

render(vNode, container) {
// Clear container
container.innerHTML = '';

// Create real DOM from virtual DOM


const realNode = this.createRealNode(vNode);
container.appendChild(realNode);
}

createRealNode(vNode) {
if (typeof vNode === 'string' || typeof vNode ===
'number') {
return document.createTextNode(vNode);
}

const { type, props, children } = 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();

const vApp = vdom.createElement('div', { className: 'app' },


vdom.createElement('h1', null, 'Virtual DOM Example'),
vdom.createElement('p', null, 'This is a simple virtual DOM
implementation.')
);

vdom.render(vApp, document.getElementById('app'));

16.7.3 Code Splitting and Lazy Loading

// Without code splitting


import { heavyFeature } from './heavyFeature';

function initApp() {
// heavyFeature is loaded even if not used immediately
document.getElementById('feature-
button').addEventListener('click', () => {
heavyFeature();
});
}

// With dynamic import (code splitting)


function initAppOptimized() {
document.getElementById('feature-
button').addEventListener('click', async () => {
// heavyFeature is only loaded when needed
const module = await import('./heavyFeature');
module.heavyFeature();
});
}

16.7.4 Service Workers for Caching

// Register a service worker


if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('ServiceWorker registered with
scope:', registration.scope);
})
.catch(error => {
console.error('ServiceWorker registration
failed:', error);
});
});
}

// service-worker.js
const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];

// Install event - cache assets


self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});

// Fetch event - serve from cache, fall back to network


self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}

// Clone the request


const fetchRequest = event.request.clone();

return fetch(fetchRequest).then(response => {


// Check if valid response
if (!response || response.status !== 200 ||
response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();

caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request,
responseToCache);
});

return response;
});
})
);
});

16.8 Chapter Summary and Exercises

Summary

In this chapter, we explored various techniques for optimizing JavaScript performance:

• Code Optimization: Improving loops, functions, data structures, and string


operations
• DOM Optimization: Minimizing DOM access, avoiding layout thrashing, using
DocumentFragment, and implementing event delegation
• Memory Management: Understanding garbage collection, avoiding memory leaks,
and implementing best practices
• Network Optimization: Reducing request size, caching API responses, and
batching requests
• Rendering Optimization: Minimizing reflows and repaints, using CSS transforms,
and implementing debouncing and throttling
• Advanced Techniques: Using Web Workers, implementing virtual DOM, code
splitting, and utilizing Service Workers

Performance optimization is an ongoing process that requires measurement, analysis,


and targeted improvements. By applying the techniques covered in this chapter, you can
create faster, more responsive JavaScript applications that provide a better user
experience.
Exercises

Exercise 1: Performance Measurement

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.

Exercise 2: DOM Optimization

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.

Exercise 3: Memory Management

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.

Exercise 4: Network Optimization

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.

Exercise 5: Advanced Project

Build a high-performance data grid component with the following features:

1. Virtual scrolling for handling large datasets


2. Efficient sorting and filtering without re-rendering the entire grid
3. Optimized column resizing and reordering
4. Batched API requests for data loading
5. Memory management to prevent leaks when the grid is destroyed
6. Debounced search functionality
7. Efficient rendering using a simple virtual DOM implementation
8. Performance monitoring and logging

By completing these exercises, you'll gain practical experience with JavaScript


performance optimization techniques and be well-prepared to build high-performance
web applications.

Chapter 17: Advanced Code Examples


This chapter brings together many of the advanced concepts discussed throughout the
book. We will explore several comprehensive code examples that demonstrate how to
apply design patterns, modules, asynchronous programming, modern APIs, and
optimization techniques in practical, real-world scenarios. These examples are designed
to challenge your understanding and provide a deeper insight into building
sophisticated JavaScript applications.

17.1 Example 1: A Simple Single-Page Application (SPA)


Router
SPAs rely on client-side routing to navigate between different views without full page
reloads. This example demonstrates a basic hash-based router using ES6 modules and
Promises.
17.1.1 Project Structure

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>

<script type="module" src="js/app.js"></script>


</body>
</html>

17.1.3 js/router.js

// js/router.js

// Store routes and the root element


const routes = {};
let rootElement = null;

/**
* 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) {

console.error("Root element not set. Call setRootElement


first.");
return;
}

// Get the current path from the hash, default to


/
const path = window.location.hash.slice(1) ||
/
;

// Find the matching view function


const viewFunction = routes[path];

if (viewFunction) {
try {
// Clear the current content
rootElement.innerHTML =
<p>Loading...</p>
; // Show loading state

// Execute the view function (can be async)


const viewContent = await
Promise.resolve(viewFunction());

// Render the new content


rootElement.innerHTML =

;
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);

// Handle the initial route on page load


window.addEventListener(
load
, handleRouteChange);
}
17.1.4 js/views/home.js

// 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!
);
});

return form; // Return a DOM element


}

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
;

// Set the root element where views will be rendered


const root = document.getElementById(
app-root
);
setRootElement(root);

// Define the routes


addRoute(
/
, homeView);
addRoute(
/about
, aboutView);
addRoute(
/contact
, contactView);

// Initialize the router


initializeRouter();
17.1.8 Key Concepts Demonstrated

• ES6 Modules: Code is organized into reusable modules ( router.js , view


modules).
• Asynchronous Operations: The aboutView simulates data fetching using
async/await and Promise .
• DOM Manipulation: Views can return HTML strings or DOM elements, which are
rendered into the rootElement .
• Event Handling: Listens for hashchange and load events to trigger routing.
• Error Handling: Basic error handling for missing routes or view rendering errors.
• Modularity: The router is decoupled from the specific views.

17.2 Example 2: Real-time Data Visualization with


WebSockets
This example demonstrates how to use WebSockets to receive real-time data and
visualize it using a simple charting library (we ll simulate the library).

17.2.1 Project Structure

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>

<script type="module" src="js/app.js"></script>


</body>
</html>

17.2.3 js/chart.js

// js/chart.js

let chartElement = null;


let chartData = [];
const MAX_DATA_POINTS = 50;

/**
* 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);

// Keep only the last MAX_DATA_POINTS


if (chartData.length > MAX_DATA_POINTS) {
chartData.shift();
}

renderChart();
}

/**
* Renders the chart based on the current data.
* This is a simplified simulation of chart rendering.
*/
function renderChart() {
if (!chartElement) return;

// Clear previous chart content


chartElement.innerHTML =

const maxValue = Math.max(1, ...chartData); // Avoid


division by zero
const barWidth = chartElement.clientWidth / MAX_DATA_POINTS;

chartData.forEach((value, index) => {


const barHeight = (value / maxValue) *
chartElement.clientHeight;
const bar = document.createElement(
div
);
bar.style.position =
absolute
;
bar.style.left = `${index * barWidth}px`;
bar.style.bottom =
0
;
bar.style.width = `${barWidth - 2}px`; // Small gap
between bars
bar.style.height = `${barHeight}px`;
bar.style.backgroundColor =
steelblue
;
bar.title = `Value: ${value}`;
chartElement.appendChild(bar);
});
}

// Handle window resize to re-render the chart


window.addEventListener(
resize
, () => {
// Debounce or throttle this in a real application
renderChart();
});

17.2.4 js/websocket.js

// js/websocket.js

let socket = null;


let onDataCallback = null;
let onStatusChangeCallback = null;

/**
* 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 = new WebSocket(url);

socket.onopen = () => {
updateStatus(
Connected
);
console.log(
WebSocket connection opened
);
};

socket.onmessage = (event) => {


try {
const data = JSON.parse(event.data);
if (data.type ===
dataPoint
&& typeof data.value ===
number
) {
if (onDataCallback) {
onDataCallback(data.value);
}
} else {
console.warn(
Received unexpected message format:
, data);
}
} catch (error) {
console.error(
Error parsing WebSocket message:
, error);
}
};

socket.onerror = (error) => {


updateStatus(
Error
);
console.error(
WebSocket error:
, error);
};

socket.onclose = (event) => {


updateStatus(`Disconnected: ${event.reason ||
Normal closure
} (Code: ${event.code})`);
console.log(
WebSocket connection closed:
, event);
// Optional: Implement reconnection logic here
};
}

/**
* 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
;

// Get DOM elements


const chartContainer = document.getElementById(
chart-container
);
const statusElement = document.getElementById(
status
);

// Initialize the chart


initializeChart(chartContainer);

// Define callbacks for WebSocket


function handleNewData(value) {
addDataPoint(value);
}

function handleStatusChange(status) {
statusElement.textContent = status;
}

// Connect to the WebSocket server


// Replace with your actual WebSocket server URL
// For testing, you can use a public echo server like wss://
echo.websocket.org
// or set up a simple local server.
const WEBSOCKET_URL =
wss://echo.websocket.org
; // Example echo server

connectWebSocket(WEBSOCKET_URL, handleNewData,
handleStatusChange);

// --- For Testing with Echo Server (Simulate receiving data)


---
// Since echo.websocket.org just echoes back, we
ll send messages to simulate receiving data.
let intervalId = null;
if (WEBSOCKET_URL ===
wss://echo.websocket.org
) {
setTimeout(() => {
if (statusElement.textContent ===
Connected
) {
intervalId = setInterval(() => {
const randomValue = Math.floor(Math.random() *
100);
const message = {
type:
dataPoint
,
value: randomValue
};
// Send message, the echo server will send it
back
sendMessage(message);
}, 1000);
}
}, 2000);
}

// Clean up on page unload


window.addEventListener(
unload
, () => {
if (intervalId) {
clearInterval(intervalId);
}
closeWebSocket();
});
// --- End Testing Code ---

17.2.6 Key Concepts Demonstrated

• WebSockets API: Establishing and managing a real-time, bidirectional connection


with a server.
• Event-Driven Programming: Responding to messages ( onmessage ) and
connection state changes ( onopen , onclose , onerror ).
• Data Visualization: Simulating a basic real-time chart update based on incoming
data.
• Modularity: Separating concerns into chart rendering, WebSocket handling, and
main application logic.
• Error Handling: Basic handling for WebSocket connection errors and message
parsing errors.
• Resource Management: Closing the WebSocket connection when the page
unloads.
17.3 Example 3: Offline-First Note-Taking App with
Service Workers and IndexedDB
This example builds a simple note-taking app that works offline using Service Workers
for caching and IndexedDB for data storage.

17.3.1 Project Structure

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>

<script type="module" src="js/app.js"></script>


</body>
</html>

17.3.4 js/db.js (IndexedDB Helper)

// 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;
}

const request = indexedDB.open(DB_NAME, DB_VERSION);

request.onerror = (event) => {


console.error(
Database error:
, event.target.error);
reject(
Database error:
+ event.target.error);
};

request.onsuccess = (event) => {


db = event.target.result;
console.log(
Database opened successfully
);
resolve(db);
};

request.onupgradeneeded = (event) => {


console.log(
Upgrading database...
);
const tempDb = event.target.result;
if (!tempDb.objectStoreNames.contains(STORE_NAME)) {
tempDb.createObjectStore(STORE_NAME, { keyPath:
id
, autoIncrement: true });
}
};
});
}

/**
* 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);

request.onsuccess = (event) => {


resolve(event.target.result); // Returns the new key
};

request.onerror = (event) => {


console.error(
Error adding note:
, event.target.error);
reject(
Error adding note:
+ event.target.error);
};
});
}

/**
* 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();

request.onsuccess = (event) => {


resolve(event.target.result);
};

request.onerror = (event) => {


console.error(
Error getting notes:
, event.target.error);
reject(
Error getting notes:
+ event.target.error);
};
});
}

/**
* 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();
};

request.onerror = (event) => {


console.error(
Error deleting note:
, event.target.error);
reject(
Error deleting note:
+ event.target.error);
};
});
}

17.3.5 js/ui.js

// js/ui.js
import { deleteNote } from
./db.js
;

const notesListElement = document.getElementById(


notes-list
);

/**
* 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;

const deleteButton = document.createElement(


button
);
deleteButton.textContent =
Delete
;
deleteButton.classList.add(
delete-btn
);

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
;

const noteForm = document.getElementById(


note-form
);
const noteInput = document.getElementById(
note-input
);

/**
* 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);
});
});
}
}

// --- Initialization ---

// Register the Service Worker


registerServiceWorker();

// Initialize UI event listeners


initializeUI(loadAndRenderNotes);

// Add event listener for the note form


noteForm.addEventListener(
submit
, handleAddNote);

// Load initial notes when the app starts


loadAndRenderNotes();

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

];

// Install event: Cache core application assets


self.addEventListener(
install
, event => {
console.log(
[Service Worker] Install
);
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log(
[Service Worker] Caching app shell
);
return cache.addAll(urlsToCache);
})
.then(() => self.skipWaiting()) // Activate worker
immediately
);
});

// Activate event: Clean up old caches


self.addEventListener(
activate
, event => {
console.log(
[Service Worker] Activate
);
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log(
[Service Worker] Removing old cache:
, cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim()) // Take control of
open clients
);
});

// Fetch event: Serve cached assets, fall back to network


(Cache-First strategy)
self.addEventListener(
fetch
, event => {
console.log(
[Service Worker] Fetch:
, event.request.url);
// For non-GET requests, and requests to other origins,
bypass the cache
if (event.request.method !==
GET
|| !event.request.url.startsWith(self.location.origin)) {
event.respondWith(fetch(event.request));
return;
}

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;
}

// Not in cache - fetch from network


console.log(
[Service Worker] Fetching from network:
, event.request.url);
return
fetch(event.request).then(networkResponse => {
// Optional: Cache the new response if
needed
// Be careful caching everything, especially
dynamic API responses
return networkResponse;
});
})
.catch(error => {
console.error(
[Service Worker] Fetch error:
, error);
// Optional: Return a fallback offline page here
})
);
});

17.3.8 Key Concepts Demonstrated

• Progressive Web App (PWA): Using manifest.json and Service Workers.


• Service Workers: Intercepting network requests ( fetch event), caching
application shell ( install event), managing cache versions ( activate event).
• IndexedDB: Storing application data (notes) locally for offline access.
• Offline-First Strategy: The application loads core assets from the cache and stores
data locally, allowing it to function without a network connection.
• Asynchronous Database Operations: Using async/await with the IndexedDB
helper module.
• Modularity: Separating database logic ( db.js ), UI updates ( ui.js ), and main
application flow ( app.js ).
17.4 Conclusion
These examples illustrate how to combine various advanced JavaScript features to build
complex and robust applications. From client-side routing and real-time updates to
offline capabilities, modern JavaScript provides the tools needed for sophisticated web
development. Remember that these are simplified examples; real-world applications
often require more extensive error handling, state management, testing, and
optimization. However, the patterns and techniques demonstrated here provide a solid
foundation for tackling more complex projects.

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.

17.5 Chapter Summary and Exercises

Summary

This chapter provided advanced code examples demonstrating the integration of


multiple JavaScript concepts:

• 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.

These examples highlight how to structure complex applications, manage asynchronous


operations, interact with browser APIs, and build modern user experiences.

Exercises

Exercise 1: Enhance the SPA Router

1. Add support for route parameters (e.g., /users/:id ).


2. Implement route guards or middleware (e.g., check if a user is logged in before
accessing a route).
3. Add transitions between view changes.
4. Implement lazy loading for view modules using dynamic import() .
5. Integrate the History API ( pushState , popstate ) instead of hash-based routing.
Exercise 2: Improve the Real-time Chart

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.

Exercise 3: Extend the Offline Notes App

1. Add note editing functionality.


2. Implement synchronization with a server when online (using Background Sync API
if possible).
3. Add search/filtering capabilities for notes.
4. Implement rich text editing for notes.
5. Improve the offline fallback experience (e.g., custom offline page).

Exercise 4: Build a Performance-Optimized Component

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.

Exercise 5: Create a Custom Promise-Based Flow Control Library

1. Build a small library for managing complex asynchronous workflows.


2. Implement functions like parallel , series , retry , timeout for Promise-
based tasks.
3. Allow tasks to pass data between them.
4. Implement cancellation support for workflows.
5. Write unit tests for your library.

Appendix A: JavaScript Best Practices


This appendix provides a collection of best practices for writing clean, efficient, and
maintainable JavaScript code. Following these guidelines will help you avoid common
pitfalls and produce higher-quality code.
A.1 Code Style and Formatting

A.1.1 Consistent Indentation and Formatting

// Good: Consistent indentation (2 or 4 spaces)


function calculateTotal(items) {
let total = 0;

for (const item of items) {


total += item.price;
}

return total;
}

// Bad: Inconsistent indentation


function calculateTotal(items) {
let total = 0;
for (const item of items) {
total += item.price;
}
return total;
}

A.1.2 Meaningful Variable and Function Names

// Good: Descriptive names


const userAccountBalance = 1250.75;
function calculateMonthlyPayment(principal, interestRate,
years) {
// Implementation
}

// Bad: Vague names


const uab = 1250.75;
function calc(p, i, y) {
// Implementation
}

A.1.3 Use Semicolons Consistently

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);

// Without semicolons (not recommended for beginners)


const name = 'John'
const greeting = `Hello, ${name}`
console.log(greeting)

A.2 Variable Declarations

A.2.1 Use const and let, Avoid var

// Good: Use const for values that won't change


const API_URL = 'https://api.example.com';
const TAX_RATE = 0.07;

// Good: Use let for values that will change


let counter = 0;
let isLoading = true;

// Bad: Using var (has function scope, not block scope)


var result = 0;

A.2.2 Declare Variables at the Top of Their Scope

// Good: Variables declared at the top of their scope


function processOrder(order) {
const taxRate = 0.07;
let subtotal = 0;
let total = 0;

// Rest of the function


subtotal = calculateSubtotal(order);
total = subtotal * (1 + taxRate);

return total;
}

// Bad: Variables scattered throughout the function


function processOrder(order) {
const taxRate = 0.07;

// Some code...

let subtotal = calculateSubtotal(order);

// More code...
let total = subtotal * (1 + taxRate);

return total;
}

A.2.3 Avoid Global Variables

// Bad: Global variables


let userId = 42;
let userName = 'John';

function updateUser() {
userId = 43;
userName = 'Jane';
}

// Good: Encapsulated in a module or object


const userModule = (function() {
let userId = 42;
let userName = 'John';

function updateUser(id, name) {


userId = id;
userName = name;
}

function getUserInfo() {
return { id: userId, name: userName };
}

return {
updateUser,
getUserInfo
};
})();

A.3 Functions

A.3.1 Keep Functions Small and Focused

// Good: Small, focused function


function calculateTax(amount, taxRate) {
return amount * taxRate;
}
// Bad: Function doing too many things
function processOrder(order) {
// Validate order
if (!order.items || order.items.length === 0) {
throw new Error('Order has no items');
}

// 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
};
}

// Better: Split into smaller functions


function validateOrder(order) {
if (!order.items || order.items.length === 0) {
throw new Error('Order has no items');
}
}

function calculateSubtotal(items) {
return items.reduce((sum, item) => sum + item.price *
item.quantity, 0);
}

function applyDiscounts(subtotal, discountCode) {


if (discountCode === 'SAVE10') {
return subtotal * 0.9;
}
return subtotal;
}

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);

let subtotal = calculateSubtotal(order.items);


subtotal = applyDiscounts(subtotal, order.discountCode);

const tax = calculateTax(subtotal);


const total = subtotal + tax;

updateInventoryForOrder(order.items);

return { subtotal, tax, total };


}

A.3.2 Use Default Parameters

// Good: Default parameters


function createUser(name, role = 'user', active = true) {
return {
name,
role,
active,
createdAt: new Date()
};
}

// 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

// Good: Return early to avoid deep nesting


function getUserPermissions(user) {
if (!user) {
return [];
}

if (!user.active) {
return [];
}

if (user.role === 'admin') {


return ['read', 'write', 'delete', 'admin'];
}

if (user.role === 'editor') {


return ['read', 'write'];
}

return ['read'];
}

// Bad: Deep nesting


function getUserPermissions(user) {
if (user) {
if (user.active) {
if (user.role === 'admin') {
return ['read', 'write', 'delete', 'admin'];
} else if (user.role === 'editor') {
return ['read', 'write'];
} else {
return ['read'];
}
} else {
return [];
}
} else {
return [];
}
}
A.4 Objects and Arrays

A.4.1 Use Object and Array Destructuring

// Good: Object destructuring


function displayUser(user) {
const { name, email, role } = user;
console.log(`Name: ${name}, Email: ${email}, Role: ${role}
`);
}

// Good: Array destructuring


const coordinates = [10, 20];
const [x, y] = coordinates;
console.log(`X: ${x}, Y: ${y}`);

// Good: Parameter destructuring


function displayUser({ name, email, role }) {
console.log(`Name: ${name}, Email: ${email}, Role: ${role}
`);
}

A.4.2 Use Spread Operator for Shallow Copies

// Good: Creating a copy of an array


const originalArray = [1, 2, 3];
const copy = [...originalArray];

// Good: Creating a copy of an object


const originalObject = { a: 1, b: 2 };
const copy = { ...originalObject };

// Good: Merging objects


const defaults = { theme: 'light', fontSize: 16 };
const userPreferences = { theme: 'dark' };
const settings = { ...defaults, ...userPreferences };
// Result: { theme: 'dark', fontSize: 16 }

A.4.3 Use Array Methods Instead of Loops

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

// Good: Using array methods


const doubled = numbers.map(n => n * 2);
const even = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((total, n) => total + n, 0);
// Less ideal: Using for loops
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}

const even = [];


for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
even.push(numbers[i]);
}
}

let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}

A.5 Error Handling

A.5.1 Use try/catch for Error Handling

// Good: Using try/catch


async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);

if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}

return await response.json();


} catch (error) {
console.error('Error fetching user data:', error);
// Handle the error appropriately
throw error; // Re-throw if needed
}
}

A.5.2 Create Custom Error Classes

// Good: Custom error classes


class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}

class AuthenticationError extends Error {


constructor(message) {
super(message);
this.name = 'AuthenticationError';
}
}

// 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.6 Asynchronous Code

A.6.1 Use Async/Await Over Promise Chains

// Good: Using async/await


async function getUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Less readable: Using Promise chains
function getUserData(userId) {
return fetchUser(userId)
.then(user => {
return fetchPosts(user.id)
.then(posts => {
return { user, posts };
});
})
.catch(error => {
console.error('Error:', error);
throw error;
});
}

A.6.2 Handle Promise Rejections

// Good: Handling rejections with async/await


async function fetchData() {
try {
const result = await fetch('/api/data');
return await result.json();
} catch (error) {
console.error('Fetch error:', error);
// Handle the error or re-throw
}
}

// Good: Handling rejections with promises


fetch('/api/data')
.then(response => response.json())
.then(data => {
// Process data
})
.catch(error => {
console.error('Fetch error:', error);
// Handle the error
});

A.7 Performance

A.7.1 Avoid Excessive DOM Manipulation

// Bad: Multiple DOM manipulations


function addItems(items) {
const list = document.getElementById('itemList');

for (const item of items) {


const li = document.createElement('li');
li.textContent = item;
list.appendChild(li); // DOM manipulation in each
iteration
}
}

// Good: Using DocumentFragment


function addItems(items) {
const list = document.getElementById('itemList');
const fragment = document.createDocumentFragment();

for (const item of items) {


const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
}

list.appendChild(fragment); // Single DOM manipulation


}

A.7.2 Debounce Event Handlers

// Debounce function
function debounce(func, delay) {
let timeoutId;

return function(...args) {
clearTimeout(timeoutId);

timeoutId = setTimeout(() => {


func.apply(this, args);
}, delay);
};
}

// Usage
const searchInput = document.getElementById('search');

// Bad: Without debounce


searchInput.addEventListener('input', function() {
searchAPI(this.value); // Called on every keystroke
});

// Good: With debounce


searchInput.addEventListener('input', debounce(function() {
searchAPI(this.value); // Called after typing stops for
300ms
}, 300));

A.7.3 Use Web Workers for CPU-Intensive Tasks

// 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

A.8.1 Sanitize User Input

// Bad: Directly inserting user input into HTML


function displayComment(comment) {
const container = document.getElementById('comments');
container.innerHTML += `<div class="comment">${comment}</
div>`;
}

// Good: Sanitizing user input


function displayComment(comment) {
const container = document.getElementById('comments');
const div = document.createElement('div');
div.className = 'comment';
div.textContent = comment; // Automatically escapes HTML
container.appendChild(div);
}
A.8.2 Avoid eval() and new Function()

// Bad: Using eval


function calculateFromUserInput(input) {
return eval(input); // Dangerous!
}

// Good: Using safer alternatives


function calculateFromUserInput(input) {
// Use a proper expression parser or limit the input format
// This is a simplified example
const allowedPattern = /^[0-9+\-*/. ]+$/;

if (!allowedPattern.test(input)) {
throw new Error('Invalid input');
}

// Still not perfect, but better than raw eval


return Function(`"use strict"; return (${input})`)();
}

// Best: Use a dedicated math expression parser library

A.8.3 Use HTTPS for External Resources

<!-- Bad: Using HTTP -->


<script src="http://example.com/script.js"></script>

<!-- Good: Using HTTPS -->


<script src="https://example.com/script.js"></script>

A.9 Testing

A.9.1 Write Testable Code

// Bad: Hard to test


function processData() {
const data = fetchDataFromServer();
const processed = data.map(item => item.value * 2);
saveToDatabase(processed);
}

// Good: Testable with dependency injection


function processData(fetchFn, saveFn) {
return async () => {
const data = await fetchFn();
const processed = data.map(item => item.value * 2);
return saveFn(processed);
};
}

// 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]);
});

A.9.2 Use Assertions

// Good: Using assertions


function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}

if (b === 0) {
throw new Error('Cannot divide by zero');
}

return a / b;
}

A.10 Documentation

A.10.1 Use JSDoc Comments

/**
* 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);
}

A.10.2 Include Examples in Documentation

/**
* 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.

Appendix B: Debugging Techniques


Debugging is an essential skill for JavaScript developers. This appendix covers various
techniques and tools for identifying and fixing bugs in your JavaScript code.

B.1 Understanding Common JavaScript Errors


Before diving into debugging techniques, it's helpful to understand the common types of
errors you might encounter.

B.1.1 Syntax Errors

Syntax errors occur when your code violates JavaScript's grammar rules. These errors
prevent your code from running at all.

// Missing closing parenthesis


function calculateTotal(price, quantity {
return price * quantity;
}

// Incorrect object literal syntax


const user = {
name: "John",
email: "[email protected]"
age: 30 // Missing comma
};
B.1.2 Reference Errors

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

// Calling an undefined function


calculateDiscount(); // ReferenceError: calculateDiscount is not
defined

// Common mistake: using a variable before declaration


console.log(total); // ReferenceError or undefined (depending on
if using var)
let total = 100;

B.1.3 Type Errors

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

// Using a method on null or undefined


const data = null;
data.process(); // TypeError: Cannot read property 'process' of
null

// Incorrect argument types


"hello" - 5; // NaN (not a TypeError, but a logical error)

B.1.4 Logic Errors

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 Console-Based Debugging


The browser's console provides several methods for debugging.

B.2.1 console.log()

The most basic debugging technique is to print values to the console.

function calculateTotal(price, quantity, discount) {


console.log("Inputs:", { price, quantity, discount });

const subtotal = price * quantity;


console.log("Subtotal:", subtotal);

const total = subtotal * (1 - discount);


console.log("Total after discount:", total);

return total;
}

B.2.2 console.table()

For arrays and objects, console.table() provides a more readable output.

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()

For DOM elements or complex objects, console.dir() shows all properties.


const button = document.querySelector('#submit-button');
console.dir(button); // Shows all properties of the button
element

B.2.4 console.trace()

To see the call stack at a specific point in your code:

function function1() {
function2();
}

function function2() {
function3();
}

function function3() {
console.trace(); // Shows the call stack
}

function1();

B.2.5 console.time() and console.timeEnd()

For performance debugging:

console.time('arrayProcessing');

const largeArray = new Array(1000000).fill(0).map((_, i) => i);


const processed = largeArray.filter(num => num % 2 ===
0).map(num => num * 2);

console.timeEnd('arrayProcessing'); // Outputs: arrayProcessing:


123.45ms

B.2.6 console.assert()

For conditional debugging:

function processPayment(amount) {
console.assert(amount > 0, 'Payment amount must be
positive');

// Process payment...
}
processPayment(-10); // Assertion failed: Payment amount must be
positive

B.3 Breakpoints and the Debugger Statement

B.3.1 Using the debugger Statement

The debugger statement pauses execution and opens the browser's debugging tools.

function calculateDiscount(price, code) {


let discount = 0;

if (code === 'SAVE10') {


discount = 0.1;
} else if (code === 'SAVE20') {
discount = 0.2;
}

debugger; // Execution will pause here when dev tools are


open

return price * (1 - discount);


}

B.3.2 Setting Breakpoints in Browser DevTools

1. Open your browser's DevTools (F12 or Ctrl+Shift+I)


2. Navigate to the Sources panel
3. Find your JavaScript file
4. Click on the line number where you want to set a breakpoint

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

B.3.3 Conditional Breakpoints

In browser DevTools, you can set breakpoints that only trigger when a condition is met:

1. Right-click on a line number


2. Select "Add conditional breakpoint"
3. Enter a condition (e.g., i === 5 or user.role === 'admin' )
B.3.4 XHR/Fetch Breakpoints

You can also set breakpoints that trigger when specific network requests are made:

1. In Chrome DevTools, go to the Sources panel


2. In the right sidebar, expand "XHR/fetch Breakpoints"
3. Add a breakpoint for a specific URL pattern

B.4 Browser DevTools Features


Modern browsers offer powerful debugging tools beyond just breakpoints.

B.4.1 Watch Expressions

In the debugging panel, you can add expressions to watch:

1. While paused at a breakpoint, find the "Watch" section


2. Click "+" to add an expression (e.g., user.name or total * tax )
3. The value will update as you step through code

B.4.2 Call Stack

The call stack shows the path of execution that led to the current point:

1. While paused at a breakpoint, find the "Call Stack" section


2. Each entry represents a function call
3. Click on an entry to see the code at that point in the execution

B.4.3 Scope Variables

The Scope panel shows all variables in the current scope:

1. While paused at a breakpoint, find the "Scope" section


2. It shows Local, Closure, and Global variables
3. You can inspect and even modify values during debugging

B.4.4 DOM Breakpoints

You can set breakpoints that trigger when the DOM is modified:

1. In DevTools, right-click on a DOM element


2. Select "Break on..." and choose:
3. Subtree modifications
4. Attribute modifications
5. Node removal

B.4.5 Event Listener Breakpoints

Break when specific events occur:

1. In DevTools, go to the Sources panel


2. In the right sidebar, expand "Event Listener Breakpoints"
3. Select events to break on (e.g., "click", "load", "error")

B.5 Advanced Debugging Techniques

B.5.1 Source Maps

Source maps allow you to debug transpiled or minified code as if it were the original
source code.

// In your webpack config


module.exports = {
// ...
devtool: 'source-map', // Generates source maps
// ...
};

B.5.2 Remote Debugging

For debugging on mobile devices or other browsers:

1. For Android: Use Chrome's remote debugging feature


2. Connect your Android device via USB
3. Enable USB debugging on the device
4. In Chrome, navigate to chrome://inspect

5. Select your device and the page to debug

6. For iOS: Use Safari's Web Inspector

7. Enable Web Inspector on your iOS device


8. Connect the device to your Mac
9. In Safari, enable the Develop menu
10. Select your device and the page to debug
B.5.3 Monitoring Events

To debug event-related issues:

// Monitor all click events


monitorEvents(document.body, 'click');

// Monitor all events on a specific element


const button = document.querySelector('#submit-button');
monitorEvents(button);

// Stop monitoring
unmonitorEvents(button);

B.5.4 Debugging Asynchronous Code

For promises and async/await:

1. Use async/await to make asynchronous code more debuggable


2. Set breakpoints inside .then() or .catch() handlers
3. Use the "Async" option in the call stack to see async call chains

async function fetchUserData(userId) {


try {
const response = await fetch(`/api/users/${userId}`);
debugger; // You can set breakpoints here
const data = await response.json();
return data;
} catch (error) {
debugger; // And here
console.error('Error fetching user data:', error);
throw error;
}
}

B.6 Common Debugging Patterns

B.6.1 Debugging Loops

When debugging loops, it's often helpful to log the iteration number and relevant values:

const items = [/* array of items */];

for (let i = 0; i < items.length; i++) {


console.log(`Iteration ${i}:`, items[i]);

// Process item...

if (/* some condition */) {


console.log(`Found issue at index ${i}`);
debugger;
}
}

B.6.2 Debugging Event Handlers

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...
});

B.6.3 Debugging API Calls

For API calls, log the request and response:

async function fetchData(url) {


console.log(`Fetching data from: ${url}`);

try {
const response = await fetch(url);
console.log('Response status:', response.status);

const data = await response.json();


console.log('Response data:', data);

return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
B.6.4 Isolating the Problem

When facing a complex bug:

1. Create a minimal reproduction of the issue


2. Comment out sections of code to isolate the problem
3. Use binary search: comment out half the code, see if the issue persists, then narrow
down

function complexFunction() {
// Part 1
// ...

// Part 2
// ...

// Part 3
// ...
}

// Debugging approach:
function complexFunction() {
// Part 1
// ...

console.log('After Part 1'); // Check if everything is


correct here

// Part 2
// Comment out to see if the issue is in Part 2
// ...

console.log('After Part 2'); // Check if everything is


correct here

// Part 3
// ...
}

B.7 Debugging Tools and Extensions

B.7.1 Browser Extensions

Several browser extensions can enhance your debugging experience:

1. React Developer Tools: For debugging React applications


2. Redux DevTools: For debugging Redux state
3. Vue.js DevTools: For debugging Vue applications
4. Augury: For debugging Angular applications

B.7.2 Logging Libraries

For more advanced logging:

// Using a library like debug


import debug from 'debug';

// Create namespaced debuggers


const logApp = debug('app');
const logAPI = debug('app:api');
const logUI = debug('app:ui');

logApp('Application starting');
logAPI('Fetching user data');
logUI('Rendering dashboard');

// Enable in browser with: localStorage.debug = 'app*'

B.7.3 Error Tracking Services

For production debugging:

// Using a service like Sentry


import * as Sentry from '@sentry/browser';

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

B.8.1 Reproduce First

Before diving into debugging:

1. Clearly define the expected behavior


2. Document the steps to reproduce the issue
3. Verify that you can consistently reproduce the problem

B.8.2 Use Version Control

Version control helps with debugging:

1. Make small, focused commits


2. Use git bisect to find which commit introduced a bug
3. Create a branch for debugging complex issues

B.8.3 Rubber Duck Debugging

Sometimes explaining the problem out loud helps:

1. Explain the issue to someone else (or a rubber duck)


2. Walk through the code line by line
3. Describe what each part should do and what it's actually doing

B.8.4 Take Breaks

When stuck on a difficult bug:

1. Step away from the problem for a while


2. Return with fresh eyes
3. Consider alternative approaches

B.8.5 Document Your Findings

After fixing a bug:

1. Document the root cause


2. Explain the solution
3. Add tests to prevent regression
4. Share knowledge with your team
B.9 Chapter Summary
Debugging is both an art and a science. This appendix covered:

• Understanding common JavaScript errors


• Using console methods for debugging
• Working with breakpoints and the debugger statement
• Leveraging browser DevTools features
• Applying advanced debugging techniques
• Following common debugging patterns
• Using debugging tools and extensions
• Following 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.

Appendix C: Resources for Further


Learning
Learning JavaScript is an ongoing journey. This appendix provides a curated list of
resources to help you continue expanding your JavaScript knowledge and skills.

C.1 Official Documentation

C.1.1 JavaScript Language

• 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.1.2 Web APIs

• MDN Web API Reference - Documentation for all browser APIs.


• HTML Living Standard - The official HTML specification.
• CSS Specifications - Official CSS specifications.
C.1.3 Popular Libraries and Frameworks

• React Documentation - Official React documentation.


• Vue.js Documentation - Official Vue.js documentation.
• Angular Documentation - Official Angular documentation.
• Node.js Documentation - Official Node.js documentation.

C.2 Online Learning Platforms

C.2.1 Free Resources

• freeCodeCamp - Free coding bootcamp with interactive lessons and projects.


• The Odin Project - Free full-stack curriculum with a focus on JavaScript.
• JavaScript.info - Modern JavaScript tutorial with detailed explanations.
• Eloquent JavaScript - Free online book by Marijn Haverbeke.
• You Don't Know JS - Book series by Kyle Simpson diving deep into JavaScript.

C.2.2 Paid Courses and Platforms

• Udemy - Marketplace for courses, including many on JavaScript.


• Pluralsight - Technology learning platform with skill assessments.
• Frontend Masters - In-depth courses by industry experts.
• Egghead.io - Concise, focused video tutorials.
• Wes Bos Courses - Popular JavaScript courses by Wes Bos.

C.3 Blogs and Newsletters

C.3.1 Blogs

• CSS-Tricks - Despite the name, covers many JavaScript topics.


• 2ality - JavaScript and ECMAScript blog by Dr. Axel Rauschmayer.
• David Walsh Blog - JavaScript tutorials and tips.
• Smashing Magazine - In-depth articles on JavaScript and web development.
• web.dev - Google's platform for web development best practices.

C.3.2 Newsletters

• JavaScript Weekly - Weekly roundup of JavaScript news and articles.


• Frontend Focus - Weekly newsletter about HTML, CSS, and browser technology.
• Node Weekly - Weekly Node.js news and articles.
• React Status - Weekly React and React Native newsletter.
C.4 Community and Forums

C.4.1 Q&A and Discussion

• Stack Overflow - Q&A site for programming questions.


• Reddit - r/javascript - JavaScript subreddit.
• Reddit - r/learnjavascript - Subreddit for JavaScript beginners.
• DEV Community - Community of software developers with JavaScript articles.

C.4.2 Chat and Real-time Help

• Discord - JavaScript - JavaScript Discord server.


• Slack - various JavaScript communities - Find JavaScript-related Slack channels.

C.5 Tools and Playgrounds

C.5.1 Online Code Editors

• CodePen - Social development environment for front-end designers and


developers.
• JSFiddle - Test your JavaScript, CSS, HTML online.
• CodeSandbox - Online code editor for web applications.
• StackBlitz - Online IDE for web applications.
• Replit - Collaborative browser-based IDE.

C.5.2 Development Tools

• GitHub - Host and review code, manage projects.


• VS Code - Popular code editor with excellent JavaScript support.
• ESLint - JavaScript linting utility.
• Prettier - Code formatter.
• Chrome DevTools - Web developer tools built into Chrome.

C.6 Books

C.6.1 For Beginners

• "JavaScript: The Definitive Guide" by David Flanagan


• "Head First JavaScript Programming" by Eric Freeman and Elisabeth Robson
• "JavaScript & jQuery: Interactive Front-End Web Development" by Jon Duckett
• "A Smarter Way to Learn JavaScript" by Mark Myers

C.6.2 For Intermediate and Advanced Developers

• "JavaScript: The Good Parts" by Douglas Crockford


• "Secrets of the JavaScript Ninja" by John Resig and Bear Bibeault
• "Programming JavaScript Applications" by Eric Elliott
• "Effective JavaScript: 68 Specific Ways to Harness the Power of JavaScript" by
David Herman
• "JavaScript Patterns" by Stoyan Stefanov

C.7 Open Source Projects to Learn From


Examining well-written open source projects is an excellent way to improve your
JavaScript skills:

• Lodash - A modern JavaScript utility library


• Express - Web framework for Node.js
• Vue.js - Progressive JavaScript framework
• React - JavaScript library for building user interfaces
• D3.js - Data visualization library

C.8 Podcasts
• JavaScript Jabber
• Syntax
• JS Party
• The Frontend Podcast
• React Podcast

C.9 Conferences and Events


Attending conferences, even virtually, can provide valuable learning opportunities and
networking:

• JSConf
• NodeConf
• React Conf
• VueConf
• SmashingConf
C.10 Coding Challenges and Practice
Regular practice is key to mastering JavaScript:

• LeetCode - Coding challenges with a focus on algorithms


• HackerRank - Coding challenges and competitions
• Codewars - Improve your skills by training with others
• Exercism - Code practice and mentorship
• JavaScript30 - 30 day vanilla JS coding challenge

C.11 Career Resources


For those looking to advance their career in JavaScript development:

• LinkedIn Learning - Professional courses on JavaScript and related technologies


• Glassdoor - Research companies and salaries
• Indeed - Job search platform
• AngelList - Startup jobs
• GitHub Jobs - Tech-focused job board

C.12 Staying Current


JavaScript and its ecosystem evolve rapidly. Here are some ways to stay up-to-date:

• Follow influential JavaScript developers on Twitter/X


• Subscribe to JavaScript newsletters
• Join local or online JavaScript meetups
• Participate in open source projects
• Regularly read the TC39 proposals repository

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!

You might also like