JavaScript Mastery_ From Basics to Advanced Techniques
JavaScript Mastery_ From Basics to Advanced Techniques
Table of Contents
1. Introduction to JavaScript
2. JavaScript Fundamentals
3. Functions in JavaScript
4. Objects and Object-Oriented Programming
5. Arrays and Array Methods
6. DOM Manipulation
7. Asynchronous JavaScript
8. Error Handling
9. ES6+ Features
10. JavaScript Best Practices
1. Introduction to JavaScript
What is JavaScript?
Unlike HTML and CSS, which are markup and styling languages respectively, JavaScript
is a full-fledged programming language that allows developers to implement complex
features on web pages. When a web page does more than just sit there and display static
information—displaying timely content updates, interactive maps, animated graphics,
scrolling video jukeboxes, etc.—JavaScript is likely involved.
JavaScript runs on the client side of the web, which means it can be executed directly in
the user's browser without needing to communicate with the server. This makes web
applications faster and more responsive to user interactions.
History and Evolution of JavaScript
JavaScript was created by Brendan Eich in just 10 days in May 1995 while he was
working at Netscape Communications Corporation. It was originally named "Mocha," then
renamed to "LiveScript," and finally to "JavaScript" when Netscape and Sun Microsystems
(the creators of Java) formed a license agreement.
Despite its name, JavaScript has very little to do with the Java programming language.
The similar name was primarily a marketing decision, as Java was very popular at the
time.
Each new version has added features and capabilities, making JavaScript more powerful
and developer-friendly.
JavaScript vs ECMAScript
Many people use the terms "JavaScript" and "ECMAScript" interchangeably, but they're
not exactly the same thing:
ECMAScript is the official name of the language specification. It's maintained by ECMA
International (European Computer Manufacturers Association) and defines how the
language should work. When people refer to "ES6" or "ES2015," they're talking about
specific versions of the ECMAScript specification.
JavaScript is the most popular implementation of the ECMAScript specification. It's what
actually runs in web browsers. While JavaScript follows the ECMAScript standard, it also
includes additional features that aren't part of the specification, particularly browser-
specific APIs like the Document Object Model (DOM) for manipulating web pages.
Think of ECMAScript as the blueprint and JavaScript as the actual building constructed
from that blueprint, with some additional features added by the builders (browser
vendors).
Basic Setup:
1. Text Editor or IDE: You need a good code editor. Popular options include:
2. Visual Studio Code (free, highly recommended)
3. Sublime Text
4. Atom
6. Web Browser: Modern browsers have built-in developer tools that are essential for
JavaScript development:
7. Google Chrome
8. Mozilla Firefox
9. Microsoft Edge
10. Safari
1. Node.js and npm: Node.js allows you to run JavaScript outside the browser, and
npm (Node Package Manager) helps you manage dependencies.
3. Build Tools: Tools like Webpack, Babel, or Parcel help bundle, transpile, and
optimize your code.
4. Linters and Formatters: ESLint and Prettier help maintain code quality and
consistency.
5. Testing Frameworks: Jest, Mocha, or Jasmine for writing and running tests.
<!DOCTYPE html>
<html>
<head>
<title>My Web Page</title>
<script>
// JavaScript code goes here
function greet() {
alert('Hello, World!');
}
</script>
</head>
<body>
<button onclick="greet()">Click Me</button>
</body>
</html>
2. External JavaScript (linking to a separate .js file):
<!DOCTYPE html>
<html>
<head>
<title>My Web Page</title>
<!-- Link to an external JavaScript file -->
<script src="script.js"></script>
</head>
<body>
<button onclick="greet()">Click Me</button>
</body>
</html>
And in script.js:
function greet() {
alert('Hello, World!');
}
<!DOCTYPE html>
<html>
<head>
<title>My Web Page</title>
</head>
<body>
<!-- Inline JavaScript in an HTML attribute -->
<button onclick="alert('Hello, World!')">Click Me</button>
</body>
</html>
1. Place scripts at the bottom of the body tag when possible to improve page loading
performance.
2. Use external JavaScript files for better organization, caching, and separation of
concerns.
3. Add the defer attribute to load scripts after HTML parsing is complete but before
the DOMContentLoaded event: ```html
```
1. Use the async attribute for scripts that don't depend on other scripts or DOM
elements: ```html
```
1. Avoid inline JavaScript for better maintainability and security (helps with Content
Security Policy).
By understanding these fundamentals, you're ready to start your journey into JavaScript
programming. The language's flexibility, ubiquity, and continuous evolution make it an
essential skill for web developers and increasingly for other types of software
development as well.
2. JavaScript Fundamentals
JavaScript syntax is the set of rules that define how JavaScript programs are constructed.
Understanding the basic syntax is essential for writing valid JavaScript code.
While semicolons are technically optional in many cases due to ASI, it's considered good
practice to include them to avoid potential issues.
Case Sensitivity
JavaScript ignores spaces, tabs, and newlines that appear in JavaScript programs. You
can use whitespace to format your code for better readability.
Comments
/* This is a
multi-line comment */
Variables are containers for storing data values. In JavaScript, there are three ways to
declare variables:
Variable Declaration
let
const
• Block-scoped
• Cannot be updated or redeclared
• Must be initialized at declaration
• For objects and arrays, the content can still be modified
1. String: Represents textual data javascript let name = "John"; let greeting
= 'Hello'; let template = `Hello, ${name}`; // Template literal
(ES6)
3. Boolean: Represents logical entities with two values: true and false javascript
let isActive = true; let isComplete = false;
4. Undefined: Represents a variable that has been declared but not assigned a value
javascript let undefinedVar; console.log(undefinedVar); // undefined
5. Null: Represents the intentional absence of any object value javascript let
emptyValue = null;
Object Type
In addition to primitive types, JavaScript has an Object type that represents a collection of
properties:
let person = {
firstName: "John",
lastName: "Doe",
age: 30,
isEmployed: true
};
Arrays, functions, and dates are all specialized types of objects in JavaScript.
// Array
let colors = ["red", "green", "blue"];
// Function
function greet() {
return "Hello!";
}
// Date
let today = new Date();
Operators
Arithmetic Operators
let counter = 5;
counter++; // Increment: counter is now 6
counter--; // Decrement: counter is now 5
Assignment Operators
// 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)
Comparison Operators
let a = 5;
let b = "5";
Logical Operators
let x = 5;
let y = 10;
Type Operators
// instanceof operator
let arr = [1, 2, 3];
console.log(arr instanceof Array); // true
JavaScript is a loosely typed language, which means variables can change types.
Explicit Type Conversion
// To string
let num = 123;
let str1 = String(num); // "123"
let str2 = num.toString(); // "123"
// To number
let str = "456";
let num1 = Number(str); // 456
let num2 = parseInt(str); // 456 (integer)
let num3 = parseFloat("3.14"); // 3.14 (float)
// To boolean
let bool1 = Boolean(1); // true
let bool2 = Boolean(0); // false
let bool3 = Boolean(""); // false
let bool4 = Boolean("hello"); // true
// String conversion
let result = "3" + 4; // "34" (number is converted to string)
// Numeric conversion
let sum = "3" - 2; // 1 (string is converted to number)
let product = "3" * 2; // 6 (string is converted to number)
// Boolean conversion
if ("hello") {
// Non-empty strings are truthy
console.log("This will execute");
}
if (0) {
// 0 is falsy
console.log("This won't execute");
}
Truthy and Falsy Values
In JavaScript, values are inherently truthy or falsy when evaluated in a boolean context:
Control Structures
Conditional Statements
Ternary Operator
switch
let day = 2;
let dayName;
switch (day) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
case 3:
dayName = "Wednesday";
break;
// ... other cases
default:
dayName = "Unknown";
}
console.log(dayName); // "Tuesday"
Loops
for Loop
Used when you know how many times you want to execute a block of code:
while Loop
let i = 0;
while (i < 5) {
console.log(i); // 0, 1, 2, 3, 4
i++;
}
do-while Loop
for...in Loop
let person = {
name: "John",
age: 30,
job: "developer"
};
for...of Loop
// continue example
for (let i = 0; i < 10; i++) {
if (i % 2 === 0) {
continue; // Skip even numbers
}
console.log(i); // 1, 3, 5, 7, 9
}
Single-line Comments
Multi-line Comments
JSDoc Comments
/**
* Calculates the sum of two numbers
* @param {number} a - The first number
* @param {number} b - The second number
* @returns {number} The sum of a and b
*/
function sum(a, b) {
return a + b;
}
1. Be concise: Write clear, brief comments that explain "why" rather than "what"
2. Keep comments updated: Outdated comments are worse than no comments
3. Comment complex logic: Focus on explaining difficult or non-obvious code
4. Use JSDoc for functions: Document parameters, return values, and purpose
5. Avoid commenting obvious code: Don't state the obvious
6. Use TODO comments: Mark areas that need future attention with // TODO:
description
By mastering these JavaScript fundamentals, you'll have a solid foundation for building
more complex applications and understanding advanced JavaScript concepts.
3. Functions in JavaScript
Function Declaration
function greet(name) {
return "Hello, " + name + "!";
}
Function Expression
Function expressions are not hoisted, so they cannot be called before they are defined.
Key Differences
Arrow Functions
Arrow functions were introduced in ES6 (ECMAScript 2015) and provide a more concise
syntax for writing functions:
// Arrow function
const add = (a, b) => a + b;
Arrow functions are ideal for: - Short callback functions - Functions that don't use this -
Functional programming patterns (map, filter, reduce)
They should be avoided for: - Methods in objects (where this refers to the object) -
Constructor functions - Event handlers where this should refer to the element
Parameters are the names listed in the function definition, while arguments are the actual
values passed to the function.
function sum(a, b) {
return a + b;
}
sum(2, 3); // 5
sum(2); // NaN (because b is undefined)
sum(2, 3, 4); // 5 (extra arguments are ignored)
You can check for missing arguments and provide default behavior:
function sum(a, b) {
// Old way (pre-ES6)
a = a || 0;
b = b || 0;
return a + b;
}
Default Parameters
Rest Parameters
The rest parameter syntax ( ... ) allows a function to accept an indefinite number of
arguments as an array:
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
sum(1, 2); // 3
sum(1, 2, 3, 4, 5); // 15
Spread Operator
The spread operator also uses the ... syntax but serves the opposite purpose of the
rest parameter. It expands an array into individual elements:
// Equivalent to console.log(1, 2, 3)
console.log(...numbers);
sum(...numbers); // 6
The spread operator can also be used to combine arrays and objects:
// Combining arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Copying arrays
const original = [1, 2, 3];
const copy = [...original];
Scope
1. Global Scope: Variables declared outside any function or block are globally
accessible.
2. Function Scope: Variables declared within a function are only accessible inside that
function.
3. Block Scope: Variables declared with let or const within a block (e.g., if
statements, loops) are only accessible within that block.
// Global scope
const globalVar = "I'm global";
function exampleFunction() {
// Function scope
const functionVar = "I'm function-scoped";
if (true) {
// Block scope
const blockVar = "I'm block-scoped";
console.log(globalVar); // Accessible
console.log(functionVar); // Accessible
console.log(blockVar); // Accessible
}
console.log(globalVar); // Accessible
console.log(functionVar); // Accessible
console.log(blockVar); // Error: blockVar is not defined
}
console.log(globalVar); // Accessible
console.log(functionVar); // Error: functionVar is not defined
Closures
A closure is a function that has access to variables from its outer (enclosing) function's
scope, even after the outer function has returned:
function createCounter() {
let count = 0; // This variable is "closed over"
return function() {
count++;
return count;
};
}
In this example, the inner function maintains access to the count variable even after
createCounter has finished executing.
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount > balance) {
return "Insufficient funds";
}
balance -= amount;
return balance;
},
getBalance: function() {
return balance;
}
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
account.getBalance(); // 120
// balance is not directly accessible
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
double(5); // 10
triple(5); // 15
function setupCounter(buttonId) {
let count = 0;
document.getElementById(buttonId).addEventListener('click', function() {
count++;
console.log(`Button clicked ${count} times`);
});
}
Callbacks
function greeting(name) {
console.log(`Hello, ${name}!`);
}
function processUserInput(callback) {
const name = prompt("Please enter your name:");
callback(name);
}
processUserInput(greeting);
Asynchronous Callbacks
fetchData(function(data) {
console.log(data); // { id: 1, name: "John" }
});
Callback Hell
fetchUserData(function(user) {
fetchUserPosts(user.id, function(posts) {
fetchPostComments(posts[0].id, function(comments) {
// Deeply nested and hard to read
console.log(comments);
});
});
});
Array.prototype.map()
Creates a new array by applying a function to each element of the original array:
Array.prototype.filter()
Array.prototype.reduce()
(function() {
console.log("This function is executed immediately");
})();
// With parameters
(function(name) {
console.log(`Hello, ${name}!`);
})("John");
Uses of IIFE
1. Creating Private Scope: Variables declared inside an IIFE are not accessible from
outside.
(function() {
const privateVar = "I am private";
console.log(privateVar); // "I am private"
})();
console.log(privateVar); // ReferenceError: privateVar is not defined
// Without IIFE
let counter = 0;
function increment() {
counter++;
}
// With IIFE
const counter = (function() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
reset: function() {
count = 0;
return count;
}
};
})();
counter.increment(); // 1
counter.increment(); // 2
counter.reset(); // 0
// Public API
return {
add: function(a, b) {
return a + b;
},
multiply: function(a, b) {
return a * b;
},
calculateArea: function(radius) {
return Math.PI * square(radius);
}
};
})();
calculator.add(2, 3); // 5
calculator.calculateArea(2); // 12.566...
calculator.square(2); // Error: square is not a public method
Objects are one of the most important data types in JavaScript. Unlike primitive data
types, objects can store multiple values as properties and methods, making them powerful
tools for organizing and structuring code.
Object Literals
const person = {
firstName: "John",
lastName: "Doe",
age: 30,
isEmployed: true,
greet: function() {
return `Hello, my name is ${this.firstName} ${this.lastName}`;
}
};
Objects are dynamic, allowing you to add, modify, or delete properties at any time:
// Deleting a property
delete person.isEmployed;
When a variable name is the same as the property name, you can use shorthand
notation:
You can use expressions as property names by wrapping them in square brackets:
Instead of defining methods using function expressions, you can use method shorthand:
// Old way
const person = {
name: "John",
greet: function() {
return `Hello, my name is ${this.name}`;
}
};
// Method shorthand
const person = {
name: "John",
greet() {
return `Hello, my name is ${this.name}`;
}
};
Property Descriptors
JavaScript objects have property descriptors that define how properties behave:
const person = {
name: "John"
};
Property Attributes
Getters and setters allow you to define special methods that are called when a property is
accessed or modified:
const person = {
firstName: "John",
lastName: "Doe",
// Getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Setter
set fullName(value) {
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
};
this Keyword
The this keyword refers to the object that is executing the current function. Its value
depends on how the function is called:
In a Method
const person = {
name: "John",
greet() {
return `Hello, my name is ${this.name}`;
}
};
In a Regular Function
In a regular function (not a method), this refers to the global object (in browsers, it's
window ):
function showThis() {
console.log(this);
}
"use strict";
function showThis() {
console.log(this);
}
showThis(); // undefined
In Event Handlers
In an event handler, this refers to the element that received the event:
document.getElementById("myButton").addEventListener("click", function() {
console.log(this); // The button element
});
In Arrow Functions
Arrow functions don't have their own this context. They inherit this from the
surrounding code:
const person = {
name: "John",
regularFunction: function() {
console.log(this.name); // "John"
setTimeout(function() {
console.log(this.name); // undefined (this refers to window/global)
}, 100);
setTimeout(() => {
console.log(this.name); // "John" (arrow function inherits this)
}, 100);
}
};
person.regularFunction();
You can explicitly set the value of this using call() , apply() , or bind() :
function greet() {
return `Hello, my name is ${this.name}`;
}
const person = { name: "John" };
Constructor Functions
Before ES6 classes, constructor functions were the primary way to create object
templates:
this.getFullName = function() {
return `${this.firstName} ${this.lastName}`;
};
}
When a function is called with the new keyword: 1. A new empty object is created 2. The
function's this is set to the new object 3. The function code executes 4. The new object
is returned (unless the function explicitly returns something else)
You can check if an object was created from a specific constructor using instanceof :
Every JavaScript object has a prototype (except for objects created with
Object.create(null) ). When you try to access a property or method, JavaScript first
looks for it on the object itself. If it doesn't find it, it looks at the object's prototype, then
that object's prototype, and so on up the prototype chain.
Object.prototype
The top of the prototype chain is Object.prototype , which provides methods like
toString() , hasOwnProperty() , etc.
Constructor Prototypes
Each constructor function has a prototype property that points to an object. Objects
created with the constructor inherit properties from this prototype:
function Person(name) {
this.name = name;
}
Adding methods to the prototype is more memory-efficient than adding them in the
constructor, as all instances share the same method.
Prototype Inheritance
Person.prototype.introduce = function() {
return `My name is ${this.name} and I am ${this.age} years old`;
};
ES6 Classes
ES6 introduced class syntax, which provides a cleaner way to create constructor functions
and manage inheritance:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
return `My name is ${this.name} and I am ${this.age} years old`;
}
}
Class Inheritance
getJobInfo() {
return `I work as a ${this.jobTitle}`;
}
Static Methods
class MathUtils {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
console.log(MathUtils.add(5, 3)); // 8
class Person {
constructor(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}
get firstName() {
return this._firstName;
}
set firstName(value) {
this._firstName = value;
}
get fullName() {
return `${this._firstName} ${this._lastName}`;
}
set fullName(value) {
const parts = value.split(" ");
this._firstName = parts[0];
this._lastName = parts[1];
}
}
Recent JavaScript versions support private class fields using the # prefix:
class BankAccount {
#balance = 0; // Private field
constructor(owner, initialBalance) {
this.owner = owner;
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount > this.#balance) {
throw new Error("Insufficient funds");
}
this.#balance -= amount;
return this.#balance;
}
get balance() {
return this.#balance;
}
}
Object Methods
const person = {
name: "John",
age: 30,
job: "Developer"
};
Object.assign()
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = Object.assign(target, source);
console.log(target); // { a: 1, b: 3, c: 4 }
console.log(result); // { a: 1, b: 3, c: 4 }
Object.create()
const personProto = {
greet() {
return `Hello, my name is ${this.name}`;
}
};
JSON (JavaScript Object Notation) is a text-based data format that resembles JavaScript
object literals but has some differences:
const person = {
name: "John",
age: 30,
password: "secret123",
birthDate: new Date(1990, 0, 1),
console.log(JSON.stringify(person));
// {"name":"John","age":30,"birthDate":"1990-01-01T00:00:00.000Z"}
const person = {
name: "John",
age: 30,
password: "secret123"
};
console.log(jsonString); // {"name":"John","age":30}
Understanding objects and object-oriented programming in JavaScript is crucial for
building complex applications. These concepts form the foundation for many JavaScript
frameworks and libraries.
Arrays are ordered collections of values that can store multiple items in a single variable.
They are one of the most commonly used data structures in JavaScript and provide
powerful methods for data manipulation.
Array Creation
// Array constructor
const numbers = new Array(1, 2, 3, 4, 5);
// Empty array
const emptyArray = [];
Modifying Arrays
Arrays in JavaScript are mutable, meaning they can be modified after creation:
// Changing an element
fruits[1] = "grape";
console.log(fruits); // ["apple", "grape", "orange"]
Array Length
JavaScript arrays come with many built-in methods that make data manipulation easier.
Copying Arrays
// Using slice()
const copy1 = original.slice();
// Using Array.from()
const copy3 = Array.from(original);
// Using concat()
const copy4 = [].concat(original);
// Using Object.assign()
const copy5 = Object.assign([], original);
Note that these methods create shallow copies. For nested arrays or objects, you'll need
deep copying techniques.
Iterating Through Arrays
for Loop
forEach() Method
for...in Loop
While this works, it's not recommended for arrays as it iterates over all enumerable
properties, not just numeric indices:
map()
filter()
reduce()
console.log(groupedByAge);
// {
// "25": ["Alice", "Charlie"],
// "30": ["Bob", "Dave"]
// }
reduceRight()
flatMap() (ES2019)
Maps each element using a mapping function, then flattens the result into a new array:
flat() (ES2019)
Creates a new array with all sub-array elements concatenated recursively up to the
specified depth:
sort()
// Descending order
numbers.sort((a, b) => b - a);
console.log(numbers); // [100, 40, 25, 10, 5]
// Sorting objects
const people = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 20 }
];
// Sort by age
people.sort((a, b) => a.age - b.age);
console.log(people);
// [
// { name: "Charlie", age: 20 },
// { name: "Alice", age: 25 },
// { name: "Bob", age: 30 }
// ]
// Sort by name
people.sort((a, b) => a.name.localeCompare(b.name));
console.log(people);
// [
// { name: "Alice", age: 25 },
// { name: "Bob", age: 30 },
// { name: "Charlie", age: 20 }
// ]
find() returns the first element that satisfies a condition, while findIndex() returns
its index:
// With objects
const people = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 20 }
];
some() checks if at least one element passes a test, while every() checks if all
elements pass:
Multidimensional Arrays
JavaScript doesn't have true multidimensional arrays, but you can create arrays of arrays:
// 2D array (matrix)
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Accessing elements
console.log(matrix[1][2]); // 6 (row 1, column 2)
// Using forEach
matrix.forEach((row, i) => {
row.forEach((value, j) => {
console.log(`matrix[${i}][${j}] = ${value}`);
});
});
// Using for...of
for (const row of matrix) {
for (const value of row) {
console.log(value);
}
}
Array-Like Objects
Some JavaScript objects look like arrays but aren't true arrays:
// DOM NodeList
const divs = document.querySelectorAll('div');
Array Destructuring
// Basic destructuring
const [first, second, third] = numbers;
console.log(first, second, third); // 1 2 3
// Skipping elements
const [a, , c] = numbers;
console.log(a, c); // 1 3
// Rest pattern
const [head, ...tail] = numbers;
console.log(head, tail); // 1 [2, 3, 4, 5]
// Default values
const [x = 0, y = 0, z = 0] = [1, 2];
console.log(x, y, z); // 1 2 0
// Swapping variables
let m = 1;
let n = 2;
[m, n] = [n, m];
console.log(m, n); // 2 1
// Combining arrays
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Copying arrays
const copy = [...arr1];
// map
const doubled = numbers.map(n => n * 2);
// filter
const even = numbers.filter(n => n % 2 === 0);
// reduce
const sum = numbers.reduce((acc, n) => acc + n, 0);
// Chaining methods
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);
Arrays are a fundamental data structure in JavaScript, and mastering array methods is
essential for effective JavaScript programming. The array methods covered in this section
provide powerful tools for data manipulation, transformation, and analysis.
6. DOM Manipulation
The Document Object Model (DOM) is a programming interface for web documents. It
represents the page so that programs can change the document structure, style, and
content. The DOM represents the document as nodes and objects; that way, programming
languages like JavaScript can interact with the page.
The DOM is not part of JavaScript; it's a Web API used by JavaScript to interact with
HTML and XML documents. It provides a structured representation of the document as a
tree of objects, where each object corresponds to a part of the document.
When a browser loads a web page, it creates a DOM tree representing that page. The
DOM tree consists of several types of nodes:
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Welcome</h1>
<p>This is a <a href="https://example.com">link</a>.</p>
</body>
</html>
DOM Tree: - Document - html - head - title - "My Page" (text node) - body - h1 -
"Welcome" (text node) - p - "This is a " (text node) - a - "link" (text node) - href="https://
example.com" (attribute node) - "." (text node)
Before you can manipulate elements, you need to select them. JavaScript provides
several methods to select DOM elements:
getElementById
getElementsByClassName
getElementsByTagName
querySelector
querySelectorAll
Returns a static NodeList of all elements that match a specified CSS selector:
// innerText is aware of styling and won't return the text of hidden elements
element.innerText = "New inner text";
Changing Attributes
// Get attribute
const href = element.getAttribute("href");
// Set attribute
element.setAttribute("href", "https://example.com");
// Remove attribute
element.removeAttribute("target");
Changing Styles
// Class manipulation
element.className = "new-class"; // Replaces all classes
element.className += " another-class"; // Appends a class (note the space)
Creating Elements
// Add attributes
newParagraph.id = "new-paragraph";
newParagraph.className = "content";
// Create a comment
const comment = document.createComment("This is a comment");
Removing Elements
// Self-removal (modern)
element.remove();
Cloning Elements
Document Fragments
Document fragments are lightweight containers that hold multiple nodes to add to the
DOM all at once, minimizing reflows and repaints:
Events are actions or occurrences that happen in the browser, which can be detected and
responded to with JavaScript.
// Basic syntax
element.addEventListener(eventType, handlerFunction, options);
// Example
const button = document.getElementById("myButton");
button.addEventListener("click", function(event) {
console.log("Button clicked!");
console.log(event); // The event object contains information about the event
});
Common Events
// Mouse events
element.addEventListener("click", handler);
element.addEventListener("dblclick", handler);
element.addEventListener("mousedown", handler);
element.addEventListener("mouseup", handler);
element.addEventListener("mousemove", handler);
element.addEventListener("mouseover", handler);
element.addEventListener("mouseout", handler);
element.addEventListener("mouseenter", handler);
element.addEventListener("mouseleave", handler);
// Keyboard events
element.addEventListener("keydown", handler);
element.addEventListener("keyup", handler);
element.addEventListener("keypress", handler);
// Form events
element.addEventListener("submit", handler);
element.addEventListener("change", handler);
element.addEventListener("input", handler);
element.addEventListener("focus", handler);
element.addEventListener("blur", handler);
// Document/Window events
window.addEventListener("load", handler);
document.addEventListener("DOMContentLoaded", handler);
window.addEventListener("resize", handler);
window.addEventListener("scroll", handler);
When an event occurs, the browser creates an event object with details about the event:
element.addEventListener("click", function(event) {
// General properties
console.log(event.type); // "click"
console.log(event.target); // The element that triggered the event
console.log(event.currentTarget); // The element the listener is attached to
console.log(event.timeStamp); // When the event occurred
// Stop propagation
event.stopPropagation();
});
Event Propagation
When an event occurs on an element, it first runs the handlers on it, then on its parent,
and so on up the tree. This is called "bubbling."
Bubbling
// HTML structure:
// <div id="outer">
// <div id="inner">
// <button id="button">Click me</button>
// </div>
// </div>
document.getElementById("button").addEventListener("click", function(event) {
console.log("Button clicked");
});
document.getElementById("inner").addEventListener("click", function(event) {
console.log("Inner div clicked");
});
document.getElementById("outer").addEventListener("click", function(event) {
console.log("Outer div clicked");
});
Capturing
Events can also be captured on their way down from the root to the target:
document.getElementById("outer").addEventListener("click", function(event) {
console.log("Outer div captured");
}, true); // true enables capturing phase
document.getElementById("inner").addEventListener("click", function(event) {
console.log("Inner div captured");
}, true);
document.getElementById("button").addEventListener("click", function(event) {
console.log("Button captured");
}, true);
Stopping Propagation
Event Delegation
Event delegation is a technique where you attach an event listener to a parent element
instead of multiple child elements. It leverages event bubbling to handle events for
multiple elements with a single listener:
// Instead of:
document.querySelectorAll("li").forEach(item => {
item.addEventListener("click", handleClick);
});
toggleButton.addEventListener("click", function() {
if (element.style.display === "none" || element.style.display === "") {
element.style.display = "block";
} else {
element.style.display = "none";
}
// Alternative using classList
// element.classList.toggle("hidden");
});
Form Validation
form.addEventListener("submit", function(event) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(emailInput.value)) {
event.preventDefault(); // Prevent form submission
errorMessage.textContent = "Please enter a valid email address";
emailInput.classList.add("error");
} else {
errorMessage.textContent = "";
emailInput.classList.remove("error");
}
});
tabs.forEach(tab => {
tab.addEventListener("click", function() {
// Remove active class from all tabs and contents
tabs.forEach(t => t.classList.remove("active"));
tabContents.forEach(content => content.classList.remove("active"));
// Add item
addButton.addEventListener("click", function() {
if (itemInput.value.trim() !== "") {
const li = document.createElement("li");
li.textContent = itemInput.value;
li.appendChild(deleteButton);
itemList.appendChild(li);
itemInput.value = "";
}
});
DOM manipulation is a fundamental skill for web development, allowing you to create
dynamic, interactive web pages. Understanding how to select, modify, create, and remove
elements, as well as how to handle events, gives you the power to build rich user
interfaces and enhance the user experience.
7. Asynchronous JavaScript
Synchronous Code
console.log("First");
console.log("Second");
console.log("Third");
// Output:
// First
// Second
// Third
Synchronous code is straightforward but can lead to problems when operations take a
long time to complete:
console.log("Start");
// This would freeze the browser if it were synchronous
const data = fetchDataFromServer(); // Imagine this takes 5 seconds
console.log("Data:", data);
console.log("End");
Asynchronous Code
console.log("Start");
setTimeout(() => {
console.log("This runs after 2 seconds");
}, 2000);
console.log("End");
// Output:
// Start
// End
// This runs after 2 seconds
Callbacks
function fetchData(callback) {
// Simulate API call with setTimeout
setTimeout(() => {
const data = { id: 1, name: "John" };
callback(data);
}, 2000);
}
console.log("Start");
fetchData(function(data) {
console.log("Data received:", data);
});
console.log("End");
// Output:
// Start
// End
// Data received: {id: 1, name: "John"}
Callback Hell
When multiple asynchronous operations depend on each other, callbacks can lead to
deeply nested code known as "callback hell" or the "pyramid of doom":
fetchUserData(function(user) {
fetchUserPosts(user.id, function(posts) {
fetchPostComments(posts[0].id, function(comments) {
fetchCommentAuthor(comments[0].authorId, function(author) {
console.log("Author:", author);
// More nested callbacks...
});
});
});
});
This code is hard to read, debug, and maintain. Promises and async/await were
introduced to solve this problem.
Promises
Creating Promises
if (success) {
resolve("Operation completed successfully!");
} else {
reject("Operation failed!");
}
}, 2000);
});
Using Promises
myPromise
.then(result => {
console.log("Success:", result);
})
.catch(error => {
console.log("Error:", error);
})
.finally(() => {
console.log("Promise settled (fulfilled or rejected)");
});
Promise States
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
// Callback-based function
function fetchDataWithCallback(callback) {
setTimeout(() => {
callback(null, { id: 1, name: "John" });
}, 2000);
}
// Promise-based version
function fetchDataWithPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, name: "John" });
// In case of error: reject(new Error("Failed to fetch data"));
}, 2000);
});
}
// Usage
fetchDataWithPromise()
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));
Promise Chaining
fetchUser(userId)
.then(user => {
console.log("User:", user);
return fetchPosts(user.id);
})
.then(posts => {
console.log("Posts:", posts);
return fetchComments(posts[0].id);
})
.then(comments => {
console.log("Comments:", comments);
})
.catch(error => {
console.error("Error:", error);
});
Promise.all
Promise.race([promise1, promise2])
.then(result => {
console.log("Fastest promise:", result); // "Second"
})
.catch(error => {
console.error("Error:", error);
});
Promise.allSettled
Promise.allSettled([promise1, promise2])
.then(results => {
console.log("All settled:", results);
// [
// { status: "fulfilled", value: "Success" },
// { status: "rejected", reason: "Failure" }
// ]
});
Promise.any (ES2021)
Async/Await
Async/await is syntactic sugar built on top of promises, making asynchronous code look
and behave more like synchronous code:
Basic Syntax
function fetchUserData() {
return fetchUser(userId)
.then(user => {
console.log("User:", user);
return fetchPosts(user.id);
})
.then(posts => {
console.log("Posts:", posts);
return fetchComments(posts[0].id);
})
.then(comments => {
console.log("Comments:", comments);
return comments;
})
.catch(error => {
console.error("Error:", error);
throw error;
});
}
After (async/await):
return comments;
} catch (error) {
console.error("Error:", error);
throw error;
}
}
While async/await makes it easy to write sequential code, sometimes you want to run
operations in parallel:
console.log("Users:", users);
console.log("Posts:", posts);
console.log("Comments:", comments);
Fetch API
The Fetch API provides a modern interface for making HTTP requests, returning
promises:
Basic Usage
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Parse JSON response
})
.then(data => {
console.log("Data:", data);
})
.catch(error => {
console.error("Fetch error:", error);
});
With Async/Await
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
POST Request
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Usage
postData('https://api.example.com/users', { name: 'John', age: 30 })
.then(data => console.log("Response:", data))
.catch(error => console.error("Error:", error));
Fetch Options
fetch('https://api.example.com/data', {
method: 'GET', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token'
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, e
body: JSON.stringify(data) // body data type must match "Content-Type" header
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Working with APIs
Modern web applications often interact with external APIs to fetch and send data:
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`API error! Status: ${response.status}`);
}
return response.json();
}
async delete(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`API error! Status: ${response.status}`);
}
return response.json();
}
}
// Usage
const api = new ApiService('https://api.example.com');
Handling Authentication
class AuthApiService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.token = localStorage.getItem('authToken');
}
setToken(token) {
this.token = token;
localStorage.setItem('authToken', token);
}
clearToken() {
this.token = null;
localStorage.removeItem('authToken');
}
getHeaders() {
const headers = {
'Content-Type': 'application/json'
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
return headers;
}
async login(credentials) {
const response = await fetch(`${this.baseUrl}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error(`Login failed! Status: ${response.status}`);
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error(`API error! Status: ${response.status}`);
}
return response.json();
}
// Usage
const authApi = new AuthApiService('https://api.example.com');
Debouncing
Debouncing limits how often a function can fire by delaying its execution until after a
certain amount of time has passed since the last invocation:
return function(...args) {
const context = this;
clearTimeout(timeoutId);
// Usage
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(event) {
console.log("Searching for:", event.target.value);
// API call or other expensive operation
}, 500);
searchInput.addEventListener('input', debouncedSearch);
Throttling
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const scrollHandler = throttle(function() {
console.log("Scroll event throttled");
// Expensive calculation or API call
}, 300);
window.addEventListener('scroll', scrollHandler);
Retry Pattern
// Exponential backoff
return fetchWithRetry(url, options, retries - 1, backoff * 2);
}
}
// Usage
fetchWithRetry('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log("Data:", data))
.catch(error => console.error("Failed after retries:", error));
// Set up timeout
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { ...options, signal })
.then(response => {
clearTimeout(timeoutId);
return response;
})
.catch(error => {
clearTimeout(timeoutId);
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));
8. Error Handling
Error handling is a critical aspect of JavaScript programming that helps create robust
applications by gracefully managing unexpected situations. Proper error handling
improves code reliability, debugging, and user experience.
try...catch Statements
try {
// Code that might throw an error
const result = riskyOperation();
console.log(result);
} catch (error) {
// Code to handle the error
console.error("An error occurred:", error.message);
}
try...catch...finally
The finally block contains code that will execute regardless of whether an exception
was thrown or caught:
try {
console.log("Trying risky operation");
throw new Error("Something went wrong");
} catch (error) {
console.error("Error caught:", error.message);
} finally {
console.log("This always executes");
}
// Output:
// Trying risky operation
// Error caught: Something went wrong
// This always executes
The finally block is useful for cleanup operations like closing files or database
connections.
Nested try...catch
You can nest try...catch blocks to handle different types of errors at different levels:
try {
try {
throw new Error("Inner error");
} catch (innerError) {
console.error("Inner catch:", innerError.message);
throw new Error("Rethrown error");
}
} catch (outerError) {
console.error("Outer catch:", outerError.message);
}
// Output:
// Inner catch: Inner error
// Outer catch: Rethrown error
throw Statement
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error("Error:", error.message);
}
// Output:
// Error: Division by zero is not allowed
You can throw any value, not just Error objects, but it's best practice to throw Error
objects:
// Not recommended
throw "Something went wrong";
throw 404;
// Recommended
throw new Error("Something went wrong");
Error Objects
JavaScript has several built-in error types that inherit from the base Error constructor:
Error
// Syntax error
try {
eval("alert('Hello world'"); // Missing closing parenthesis
} catch (error) {
console.log(error instanceof SyntaxError); // true
console.log(error.message);
}
You can create custom error types by extending the Error class:
// Usage
try {
throw new ValidationError("Invalid email format");
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation error:", error.message);
} else if (error instanceof DatabaseError) {
console.log(`Database error (${error.code}):`, error.message);
} else {
console.log("Unknown error:", error);
}
}
Promises
fetchData()
.then(data => {
console.log("Data:", data);
})
.catch(error => {
console.error("Error fetching data:", error);
});
fetchData()
.then(data => {
// This will throw an error
return JSON.parse(data);
})
.then(parsedData => {
console.log("Parsed data:", parsedData);
})
.catch(error => {
// This will catch errors from fetchData() and JSON.parse()
console.error("Error in promise chain:", error);
});
Async/Await
For callback-based APIs, error handling typically involves checking for an error parameter:
Window.onerror
window.addEventListener('unhandledrejection', function(event) {
console.error("Unhandled promise rejection:", event.reason);
try {
// Risky code
} catch (error) {
if (error instanceof TypeError) {
// Handle type errors
} else if (error instanceof NetworkError) {
// Handle network errors
} else {
// Handle other errors or rethrow
throw error;
}
}
let connection;
try {
connection = openDatabaseConnection();
// Use connection
} catch (error) {
console.error("Database error:", error);
// Handle error
} finally {
// This ensures the connection is closed even if an error occurs
if (connection) {
connection.close();
}
}
4. Provide Meaningful Error Messages
function validateUser(user) {
if (!user) {
throw new Error("User object is required");
}
if (!user.name) {
throw new ValidationError("User name is required");
}
if (!isValidEmail(user.email)) {
throw new ValidationError(`Invalid email format: ${user.email}`);
}
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("Component error:", error);
console.error("Component stack:", errorInfo.componentStack);
// Log error to an error reporting service
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
9. ES6+ Features
ECMAScript 2015 (ES6) and subsequent versions introduced numerous powerful features
that transformed JavaScript development. These modern features have made JavaScript
more expressive, concise, and powerful. This section covers the most important ES6+
features that every JavaScript developer should know.
Template Literals
Template literals provide an improved way to work with strings, allowing embedded
expressions and multi-line strings:
// Multi-line strings
const oldMultiline = "This is line 1.\n" +
"This is line 2.";
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] ? `<strong>${values[i]}</strong>` : '');
}, '');
}
Destructuring Assignment
Destructuring allows you to extract values from arrays or properties from objects into
distinct variables:
Array Destructuring
// Old way
const numbers = [1, 2, 3];
const first = numbers[0];
const second = numbers[1];
// Array destructuring
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3
// Skip elements
const [x, , z] = [1, 2, 3];
console.log(x, z); // 1 3
// Rest pattern
const [head, ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [2, 3, 4]
// Default values
const [p = 0, q = 0] = [1];
console.log(p, q); // 1 0
// Swapping variables
let m = 1;
let n = 2;
[m, n] = [n, m];
console.log(m, n); // 2 1
Object Destructuring
// Old way
const person = { name: "John", age: 30 };
const personName = person.name;
const personAge = person.age;
// Object destructuring
const { name, age } = person;
console.log(name, age); // "John" 30
// Default values
const { name, age, job = "Unknown" } = person;
console.log(job); // "Unknown"
// Nested destructuring
const user = {
id: 1,
name: "John",
address: {
city: "New York",
country: "USA"
}
};
// Old way
function printPerson(person) {
console.log(person.name, person.age);
}
// With destructuring
function printPerson({ name, age }) {
console.log(name, age);
}
printPerson(); // "Anonymous" 0
printPerson({ name: "John" }); // "John" 0
Default Parameters
Default parameters allow you to specify default values for function parameters:
// Old way
function greet(name) {
name = name || "Guest";
return "Hello, " + name;
}
The spread ( ... ) and rest operators provide powerful ways to work with arrays and
objects:
Spread Operator
The spread operator expands an iterable (like an array) into individual elements:
// Combining arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Copying arrays
const original = [1, 2, 3];
const copy = [...original];
// Overriding properties
const updated = { ...person, age: 31 };
// { name: "John", age: 31 }
Rest Operator
console.log(sum(1, 2, 3, 4, 5)); // 15
process(1, 2, 3, 4, 5);
// Rest in destructuring
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
Arrow Functions
Arrow functions provide a more concise syntax for writing functions and lexically bind the
this value:
// Traditional function expression
const add = function(a, b) {
return a + b;
};
// Arrow function
const add = (a, b) => a + b;
Lexical this
Arrow functions don't have their own this context; they inherit this from the
surrounding code:
// 2. Use bind()
const person2 = {
name: "John",
hobbies: ["reading", "music", "sports"],
printHobbies: function() {
this.hobbies.forEach(function(hobby) {
console.log(`${this.name} likes ${hobby}`);
}.bind(this));
}
};
Classes
ES6 introduced class syntax, providing a cleaner way to create constructor functions and
manage inheritance:
// ES6 class
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}`;
}
// Static method
static createAnonymous() {
return new Person("Anonymous", 0);
}
}
Class Inheritance
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
return `My name is ${this.name} and I am ${this.age} years old`;
}
}
class Person {
constructor(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}
get firstName() {
return this._firstName;
}
set firstName(value) {
this._firstName = value;
}
get lastName() {
return this._lastName;
}
set lastName(value) {
this._lastName = value;
}
get fullName() {
return `${this._firstName} ${this._lastName}`;
}
set fullName(value) {
const parts = value.split(" ");
this._firstName = parts[0];
this._lastName = parts[1];
}
}
class BankAccount {
// Public field
owner;
// Private field
#balance;
constructor(owner, initialBalance) {
this.owner = owner;
this.#balance = initialBalance;
}
// Private method
#validateAmount(amount) {
return amount > 0;
}
deposit(amount) {
if (this.#validateAmount(amount)) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount) {
if (this.#validateAmount(amount) && amount <= this.#balance) {
this.#balance -= amount;
return true;
}
return false;
}
get balance() {
return this.#balance;
}
}
Modules (import/export)
ES6 modules provide a standard way to organize and share code between JavaScript
files:
Named Exports/Imports
// math.js
export const PI = 3.14159;
// app.js
import { PI, add, subtract } from './math.js';
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
Default Exports/Imports
// person.js
export default class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, my name is ${this.name}`;
}
}
// app.js
import Person from './person.js';
Mixed Exports/Imports
// utils.js
export const VERSION = '1.0.0';
// app.js
import Utils, { VERSION, formatDate } from './utils.js';
console.log(VERSION); // "1.0.0"
console.log(formatDate(new Date())); // "4/7/2025" (depends on locale)
console.log(Utils.generateId()); // Random ID
Renaming Imports/Exports
// math.js
export const PI = 3.14159;
export const E = 2.71828;
// app.js
import { PI as pi, E as e } from './math.js';
console.log(pi); // 3.14159
console.log(e); // 2.71828
Import All
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// app.js
import * as math from './math.js';
console.log(math.PI); // 3.14159
console.log(math.add(5, 3)); // 8
ES6 introduced new data structures: Map, Set, WeakMap, and WeakSet.
Map
// Creating a Map
const userRoles = new Map();
// Adding entries
userRoles.set('john', 'admin');
userRoles.set('jane', 'editor');
userRoles.set('bob', 'subscriber');
// Alternative initialization
const userRoles = new Map([
['john', 'admin'],
['jane', 'editor'],
['bob', 'subscriber']
]);
// Getting values
console.log(userRoles.get('john')); // "admin"
// Size
console.log(userRoles.size); // 3
// Deleting entries
userRoles.delete('bob');
console.log(userRoles.size); // 2
// Objects as keys
const userMap = new Map();
const john = { name: 'John' };
const jane = { name: 'Jane' };
Set
// Adding values
uniqueNumbers.add(1);
uniqueNumbers.add(2);
uniqueNumbers.add(3);
uniqueNumbers.add(1); // Ignored (already exists)
// Alternative initialization
const uniqueNumbers = new Set([1, 2, 3, 1, 2]); // Duplicates are ignored
// Size
console.log(uniqueNumbers.size); // 3
// Deleting values
uniqueNumbers.delete(2);
console.log(uniqueNumbers.size); // 2
WeakMap and WeakSet are similar to Map and Set, but they hold "weak" references to
objects:
// WeakMap
const weakMap = new WeakMap();
let obj = { name: 'John' };
weakMap.set(obj, 'metadata');
console.log(weakMap.get(obj)); // "metadata"
// WeakSet
const weakSet = new WeakSet();
let user = { name: 'John' };
weakSet.add(user);
console.log(weakSet.has(user)); // true
Symbol
// Creating symbols
const sym1 = Symbol();
const sym2 = Symbol('description');
const sym3 = Symbol('description'); // Different from sym2
console.log(user.name); // "John"
console.log(Object.keys(user)); // ["name"] (symbols are not enumerable)
// Well-known symbols
const iterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
for (const value of iterable) {
console.log(value); // 1, 2, 3
}
Iterators
An iterator is an object that provides a next() method which returns an object with
value and done properties:
// Custom iterator
function createIterator(array) {
let index = 0;
return {
next: function() {
return index < array.length ?
{ value: array[index++], done: false } :
{ done: true };
}
};
}
// Iterable object
const iterable = {
[Symbol.iterator]: function() {
let i = 1;
return {
next: function() {
return i <= 3 ?
{ value: i++, done: false } :
{ done: true };
}
};
}
};
Generators
Generators are functions that can be paused and resumed, making it easier to create
iterators:
// Generator function
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
// Infinite generator
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
Optional Chaining
Optional chaining ( ?. ) allows you to access deeply nested object properties without
worrying about whether the property exists:
const user1 = {
name: 'John',
address: {
city: 'New York',
country: 'USA'
}
};
const user2 = {
name: 'Jane'
// No address
};
console.log(getUserCity(user1)); // "New York"
console.log(getUserCity(user2)); // undefined
console.log(getUserCity(null)); // undefined
The nullish coalescing operator ( ?? ) provides a way to specify a default value when a
value is null or undefined :
BigInt
globalThis
const promises = [
Promise.resolve("Success"),
Promise.reject("Error"),
Promise.resolve("Another success")
];
Promise.allSettled(promises).then(results => {
console.log(results);
// [
// { status: "fulfilled", value: "Success" },
// { status: "rejected", reason: "Error" },
// { status: "fulfilled", value: "Another success" }
// ]
});
String.prototype.replaceAll()
Writing clean, maintainable, and efficient JavaScript code is essential for successful
projects. This section covers best practices that will help you write better JavaScript code,
avoid common pitfalls, and improve the overall quality of your applications.
Code Organization
Proper code organization is crucial for maintainability, especially as projects grow in size
and complexity.
Break your code into small, focused modules that each handle a specific responsibility:
// ui-controller.js
import { dataService } from './data-service.js';
export const uiController = {
renderUI() { /* ... */ },
updateView() { /* ... */ }
};
// event-handler.js
import { uiController } from './ui-controller.js';
export const eventHandler = {
setupEventListeners() { /* ... */ },
handleClick() { /* ... */ }
};
// utilities.js
export const formatDate = () => { /* ... */ };
export const calculateTotal = () => { /* ... */ };
// app.js
import { dataService } from './data-service.js';
import { uiController } from './ui-controller.js';
import { eventHandler } from './event-handler.js';
const app = {
init() {
dataService.fetchData();
uiController.renderUI();
eventHandler.setupEventListeners();
}
};
Design Patterns
Module Pattern
// Public API
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
})();
Factory Pattern
Observer Pattern
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(...args));
}
}
off(event, listener) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(l => l !== listener);
}
}
}
// Usage
const emitter = new EventEmitter();
emitter.on('userCreated', user => console.log(`New user: ${user.name}`));
emitter.emit('userCreated', { name: 'John' });
Project Structure
project/
├── src/
│ ├── components/
│ │ ├── header/
│ │ │ ├── header.js
│ │ │ └── header.css
│ │ └── footer/
│ │ ├── footer.js
│ │ └── footer.css
│ ├── services/
│ │ ├── api-service.js
│ │ └── auth-service.js
│ ├── utils/
│ │ ├── date-utils.js
│ │ └── string-utils.js
│ └── app.js
├── tests/
│ ├── components/
│ ├── services/
│ └── utils/
├── dist/
├── package.json
└── README.md
Naming Conventions
// Bad
const x = getUserData();
function calc(a, b) { return a + b; }
// Good
const userData = getUserData();
function calculateTotal(price, tax) { return price + tax; }
Constants
class UserProfile {
constructor(user) {
this.user = user;
}
}
function ShoppingCart() {
this.items = [];
}
class User {
constructor(name) {
this.name = name;
this._password = null; // Private property
}
setPassword(password) {
this._password = this._hashPassword(password);
}
}
Boolean Variables
File Naming
Optimize Loops
Limit the rate at which functions execute, especially for expensive operations:
// Usage
const debouncedSearch = debounce(searchFunction, 300);
const throttledScroll = throttle(scrollHandler, 100);
window.addEventListener('input', debouncedSearch);
window.addEventListener('scroll', throttledScroll);
Memoization
function memoize(func) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = func.apply(this, args);
cache.set(key, result);
return result;
};
}
// Usage
const expensiveFunction = (a, b) => {
console.log('Computing...');
return a * b;
};
1. Circular references:
JavaScript uses two main garbage collection strategies: 1. Reference counting: Counts
references to objects 2. Mark and sweep: Identifies and removes unreachable objects
get() {
return this.pool.length > 0 ? this.pool.pop() : this.createFn();
}
release(obj) {
this.pool.push(obj);
}
}
// Usage
const pointPool = new ObjectPool(() => ({ x: 0, y: 0 }));
const point = pointPool.get();
point.x = 10;
point.y = 20;
// Use point...
pointPool.release(point);
Security Considerations
element.innerHTML = sanitizeHTML(userInput);
Avoid eval()
Secure Cookies
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
function validateForm() {
const email = document.getElementById('email').value;
if (!validateEmail(email)) {
showError('Please enter a valid email address');
return false;
}
return true;
}
Debugging Techniques
Effective debugging is essential for identifying and fixing issues in JavaScript code.
Console Methods
// Timing operations
console.time('Operation');
performOperation();
console.timeEnd('Operation');
// Tabular data
console.table([
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
]);
// Conditional logging
console.assert(x > 0, 'x must be positive');
// Stack traces
console.trace('Tracing function calls');
// Styling console output
console.log('%cHello World', 'color: blue; font-size: 20px');
Breakpoints
debugger Statement
function calculateTotal(items) {
let total = 0;
for (const item of items) {
debugger; // Execution will pause here when dev tools are open
total += item.price * item.quantity;
}
return total;
}
Source Maps
Source maps help debug minified code by mapping it back to the original source:
// In webpack config
module.exports = {
// ...
devtool: 'source-map',
// ...
};
Testing JavaScript Code
Types of Tests
Testing Frameworks
Popular JavaScript testing frameworks include: - Jest - Mocha - Jasmine - Cypress (for
E2E testing)
// Hard to test
function fetchAndProcessUserData() {
const data = fetch('/api/users').then(res => res.json());
return processData(data);
}
function processUserData(data) {
// Process the data
return transformedData;
}
// math.js
export function add(a, b) {
return a + b;
}
// math.test.js
import { add } from './math';
Follow the TDD cycle: 1. Write a failing test 2. Write the minimum code to make the test
pass 3. Refactor the code 4. Repeat
Linters
// .eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"indent": ["error", 4],
"semi": ["error", "always"],
"no-unused-vars": "warn"
}
};
Code Formatters
// .prettierrc
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid"
}
Type Checking
// With TypeScript
function add(a: number, b: number): number {
return a + b;
}
// With JSDoc
/**
* Adds two numbers together
* @param {number} a - The first number
* @param {number} b - The second number
* @returns {number} The sum of a and b
*/
function add(a, b) {
return a + b;
}
Documentation
Good documentation makes code more maintainable and easier for others to use.
JSDoc Comments
/**
* Represents a user in the system
* @class
*/
class User {
/**
* Create a user
* @param {string} name - The user's name
* @param {string} email - The user's email
* @param {Object} [options] - Optional settings
* @param {boolean} [options.isAdmin=false] - Whether the user is an admin
*/
constructor(name, email, options = {}) {
this.name = name;
this.email = email;
this.isAdmin = options.isAdmin || false;
}
/**
* Get the user's display name
* @returns {string} The formatted display name
*/
getDisplayName() {
return `${this.name} <${this.email}>`;
}
}
README Files
# Project Name
```bash
npm install project-name
Usage
API
doSomething()
License
MIT
```javascript
// Bad: Explains what the code does (obvious from the code itself)
// Increment the counter
counter++;
Feature Detection
Polyfills
Transpiling
Performance Monitoring
Web Vitals
Track Core Web Vitals metrics: - Largest Contentful Paint (LCP) - First Input Delay (FID) -
Cumulative Layout Shift (CLS)
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
Performance API
Lighthouse
Accessibility
Focus Management
element.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
});
}
ARIA Attributes
// Toggle a dropdown
function toggleDropdown(button, menu) {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
menu.hidden = isExpanded;
if (!isExpanded) {
// Focus the first item when opening
menu.querySelector('a, button').focus();
}
}
function announce(message) {
const announcer = document.getElementById('announcer');
announcer.textContent = message;
}
// Usage
announce('New items have been loaded');
By following these best practices, you'll write JavaScript code that is more maintainable,
performant, secure, and accessible. These practices will help you avoid common pitfalls
and create higher-quality applications.