-
-
Notifications
You must be signed in to change notification settings - Fork 2
JavaScript Intermediate Guide
- Introduction
- ES6+ Modern Features
- Asynchronous JavaScript
- DOM Manipulation
- Error Handling
- Functional Programming
- Working with APIs
- Practice Projects
This guide builds on the concepts from the Beginner guides. We'll cover modern JavaScript (ES6+), asynchronous programming, DOM manipulation, error handling, and functional programming concepts. By the end, you'll be ready to build real-world applications.
ECMAScript 2015 (ES6) and subsequent versions introduced many features that make JavaScript more powerful and expressive.
We've covered these in the beginner guide, but let's dive deeper:
// let - block scoped, can be reassigned
let count = 0;
count = 1; // OK
// const - block scoped, cannot be reassigned
const PI = 3.14159;
// PI = 3; // Error!
// const with objects - the reference is constant, not the contents
const user = { name: "Alice" };
user.name = "Bob"; // OK - modifying property
// user = {}; // Error - reassigning
// const with arrays
const numbers = [1, 2, 3];
numbers.push(4); // OK
// numbers = []; // Error
// Temporal Dead Zone (TDZ)
{
// console.log(x); // ReferenceError!
let x = 5;
console.log(x); // 5
}Arrow functions are more than just shorter syntax - they have different behavior with this:
// Basic arrow function
const add = (a, b) => a + b;
// Single parameter - no parentheses needed
const square = x => x * x;
// No parameters
const getRandom = () => Math.random();
// Multiple statements need braces and return
const calculate = (a, b) => {
const sum = a + b;
const product = a * b;
return { sum, product };
};
// Arrow functions and 'this'
function Timer() {
this.seconds = 0;
// Regular function - 'this' refers to the Timer instance
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}, 1000);
// Arrow function - 'this' is lexically bound
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
// Arrow functions as methods (be careful!)
const person = {
name: "Alice",
// Arrow function here loses 'this' context
greet: () => {
console.log("Hello, " + this.name); // 'this' is NOT person!
},
// Regular method
introduce() {
console.log("I am " + this.name); // Works correctly
}
};Template literals provide powerful string formatting:
// Basic interpolation
const name = "Alice";
const greeting = `Hello, ${name}!`;
console.log(greeting); // "Hello, Alice!"
// Expressions work too
const a = 5, b = 3;
console.log(`${a} + ${b} = ${a + b}`); // "5 + 3 = 8"
// Multi-line strings
const poem = `Roses are red,
Violets are blue,
Sugar is sweet,
And so are you.`;
console.log(poem);
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((result, string, i) => {
return result + string + (values[i] ? `<mark>${values[i]}</mark>` : '');
}, '');
}
const name = "Alice";
const result = highlight`Hello, ${name}! Welcome back.`;
console.log(result); // "Hello, <mark>Alice</mark>! Welcome back."Destructuring allows you to unpack values from arrays or properties from objects:
// Array destructuring
const colors = ["red", "green", "blue"];
const [first, second, third] = colors;
console.log(first); // "red"
// Skip elements
const [primary, , tertiary] = colors;
console.log(primary, tertiary); // "red", "blue"
// Rest pattern
const [head, ...tail] = colors;
console.log(head); // "red"
console.log(tail); // ["green", "blue"]
// Default values
const [a, b, c, d = "default"] = [1, 2, 3];
console.log(d); // "default"
// Swap variables
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1
// Object destructuring
const person = { name: "Alice", age: 25, city: "NYC" };
const { name, age } = person;
console.log(name, age); // "Alice", 25
// Rename variables
const { name: personName, age: personAge } = person;
console.log(personName); // "Alice"
// Default values
const { name, country = "USA" } = person;
console.log(country); // "USA"
// Nested destructuring
const data = {
user: {
profile: {
email: "alice@example.com"
}
}
};
const { user: { profile: { email } } } = data;
console.log(email); // "alice@example.com"
// In function parameters
function greet({ name, age }) {
return `Hello, ${name}! You are ${age}.`;
}
console.log(greet({ name: "Alice", age: 25 })); // "Hello, Alice! You are 25."The spread operator (...) expands iterables, while rest collects multiple elements:
// Spread with arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Copy array
const copy = [...arr1];
console.log(copy); // [1, 2, 3]
// Spread with objects
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 3, c: 4 } (b is overwritten)
// Copy object
const clone = { ...obj1 };
// In function calls
const nums = [1, 2, 3, 4, 5];
console.log(Math.max(...nums)); // 5
// Rest parameters
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
// Destructuring with rest
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
// Object destructuring with rest
const { name, ...others } = { name: "Alice", age: 25, city: "NYC" };
console.log(name); // "Alice"
console.log(others); // { age: 25, city: "NYC" }Modules allow you to organize code into separate files:
// Named exports (math.js)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// Default export (utils.js)
export default function formatCurrency(value) {
return `$${value.toFixed(2)}`;
}
// Importing named exports
import { add, subtract } from './math.js';
console.log(add(2, 3)); // 5
// Import with alias
import { add as sum } from './math.js';
console.log(sum(2, 3)); // 5
// Import all as namespace
import * as MathUtils from './math.js';
console.log(MathUtils.add(2, 3)); // 5
// Import default export
import formatCurrency from './utils.js';
console.log(formatCurrency(99.9)); // "$99.90"
// Combined import
import formatCurrency, { add } from './both.js';JavaScript is single-threaded but can handle asynchronous operations through the event loop.
Callbacks are functions passed as arguments to other functions:
// Synchronous callback
function processArray(arr, callback) {
const results = [];
for (const item of arr) {
results.push(callback(item));
}
return results;
}
const numbers = [1, 2, 3];
const doubled = processArray(numbers, x => x * 2);
console.log(doubled); // [2, 4, 6]
// Asynchronous callback (old pattern)
function fetchData(callback) {
setTimeout(() => {
callback(null, { name: "Alice", age: 25 });
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error("Error:", error);
} else {
console.log("Data:", data);
}
});
// Callback hell (avoid!)
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c);
});
});
});Promises provide a cleaner way to handle asynchronous operations:
// Creating a promise
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Alice" });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}
// Using promises
fetchUser(1)
.then(user => {
console.log("User:", user);
return fetchUser(2);
})
.then(user2 => {
console.log("User 2:", user2);
})
.catch(error => {
console.error("Error:", error.message);
})
.finally(() => {
console.log("Operation complete");
});
// Promise.all - wait for all promises
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values)); // [1, 2, 3]
// Promise.race - first one to resolve
Promise.race([
new Promise(resolve => setTimeout(resolve, 100, "fast")),
new Promise(resolve => setTimeout(resolve, 200, "slow"))
])
.then(value => console.log(value)); // "fast"
// Promise.allSettled - wait for all to settle
Promise.allSettled([
Promise.resolve(1),
Promise.reject("error"),
Promise.resolve(3)
])
.then(results => console.log(results));
// [{status: "fulfilled", value: 1}, {status: "rejected", reason: "error"}, ...]Async/await provides synchronous-looking code for asynchronous operations:
// Async function
async function getUser(id) {
return { id, name: "Alice" };
}
// Using async/await
async function main() {
try {
const user = await getUser(1);
console.log("User:", user);
} catch (error) {
console.error("Error:", error);
}
}
main();
// Await with multiple promises
async function getData() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
return { users, posts };
}
// Sequential vs parallel
async function sequential() {
const result1 = await fetch('/api/item1');
const result2 = await fetch('/api/item2');
// Takes 2x as long
}
async function parallel() {
const [result1, result2] = await Promise.all([
fetch('/api/item1'),
fetch('/api/item2')
]);
// Takes same time as slowest request
}// Try/Catch with async/await
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Fetch failed:", error);
throw error; // Re-throw for caller to handle
}
}
// Handling multiple errors
async function processAll(items) {
const results = [];
for (const item of items) {
try {
const result = await processItem(item);
results.push({ success: true, data: result });
} catch (error) {
results.push({ success: false, error: error.message });
}
}
return results;
}
// Error boundaries pattern
async function safeAsync(fn, defaultValue) {
try {
return await fn();
} catch (error) {
console.error(error);
return defaultValue;
}
}
const data = await safeAsync(() => fetchData('/api/data'), []);The Document Object Model (DOM) represents your HTML as objects that JavaScript can manipulate.
// Single elements
const element = document.getElementById('myId');
const firstDiv = document.querySelector('div');
const elementByClass = document.querySelector('.myClass');
// Multiple elements (returns NodeList - array-like)
const allDivs = document.querySelectorAll('div');
const allItems = document.getElementsByClassName('item');
// NodeList methods
allDivs.forEach(div => console.log(div));
// Convert to real array
const arrayFromNodes = Array.from(allDivs);
// Check relationships
const parent = element.parentElement;
const children = element.children;
const nextSibling = element.nextElementSibling;
const prevSibling = element.previousElementSibling;// Text content
element.textContent = "New text";
element.innerText = "Visible text";
// HTML content
element.innerHTML = "<strong>Bold text</strong>";
// Attributes
element.setAttribute('data-id', '123');
const id = element.getAttribute('data-id');
element.removeAttribute('data-id');
// CSS styles
element.style.color = 'blue';
element.style.backgroundColor = '#f0f0f0';
element.style.display = 'none';
// Classes
element.classList.add('active');
element.classList.remove('hidden');
element.classList.toggle('selected');
const hasClass = element.classList.contains('active');
// Dataset (data-* attributes)
element.dataset.userId = '123';
console.log(element.dataset.userId);// Add event listener
element.addEventListener('click', function(event) {
console.log('Clicked!', event.target);
});
// Arrow function
element.addEventListener('click', (event) => {
console.log(event.clientX, event.clientY);
});
// Remove event listener (needs named function)
function handleClick(event) {
console.log('Clicked!');
}
element.addEventListener('click', handleClick);
element.removeEventListener('click', handleClick);
// Event delegation
document.querySelector('ul').addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
console.log('List item clicked:', event.target.textContent);
}
});
// Common events
element.addEventListener('click'); // Mouse click
element.addEventListener('dblclick'); // Double click
element.addEventListener('mouseenter'); // Mouse over
element.addEventListener('mouseleave'); // Mouse out
element.addEventListener('mouseover'); // Mouse enters (bubbles)
element.addEventListener('mouseout'); // Mouse leaves (bubbles)
input.addEventListener('input'); // Value changes
input.addEventListener('change'); // Value changes (on blur)
input.addEventListener('focus'); // Gets focus
input.addEventListener('blur'); // Loses focus
document.addEventListener('keydown'); // Key pressed
document.addEventListener('keyup'); // Key released
form.addEventListener('submit'); // Form submitted// Create new element
const newDiv = document.createElement('div');
newDiv.textContent = "Hello!";
newDiv.classList.add('greeting');
// Add to DOM
parent.appendChild(newDiv); // At the end
parent.insertBefore(newDiv, parent.firstChild); // At specific position
// Modern insertion methods
parent.append(newDiv); // Multiple items
parent.prepend(newDiv); // At beginning
const reference = parent.querySelector('.some-element');
reference.before(newDiv); // Before reference
reference.after(newDiv); // After reference
// Remove element
newDiv.remove(); // Modern method
parent.removeChild(newDiv); // Old method
// Clone element
const clone = element.cloneNode(true); // Deep clone (with children)
const shallowClone = element.cloneNode(false); // Just the elementProper error handling makes your code more robust and easier to debug.
// Basic try/catch
try {
const result = riskyOperation();
console.log(result);
} catch (error) {
console.error("Something went wrong:", error.message);
}
// Try/Catch/Finally
try {
const data = JSON.parse(userInput);
processData(data);
} catch (error) {
console.error("Failed to parse:", error.message);
} finally {
console.log("Cleanup here"); // Always runs
}
// Catching specific error types
try {
// Code that might throw different errors
} catch (error) {
if (error instanceof TypeError) {
console.log("Type error:", error.message);
} else if (error instanceof RangeError) {
console.log("Range error:", error.message);
} else {
throw error; // Re-throw unknown errors
}
}// Throw built-in errors
throw new Error("Something went wrong");
throw new TypeError("Expected a string");
throw new RangeError("Value out of range");
throw new SyntaxError("Invalid syntax");
throw new ReferenceError("Variable not defined");
// Custom error class
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateAge(age) {
if (typeof age !== "number") {
throw new TypeError("Age must be a number");
}
if (age < 0 || age > 150) {
throw new ValidationError("Age must be between 0 and 150", "age");
}
return true;
}class AppError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.details = details;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, "NOT_FOUND", { resource });
}
}
class UnauthorizedError extends AppError {
constructor() {
super("You must be logged in", "UNAUTHORIZED");
}
}
// Usage
function getUser(id) {
const user = database.find(id);
if (!user) {
throw new NotFoundError("User");
}
return user;
}Functional programming is a paradigm that treats computation as the evaluation of mathematical functions.
// Pure function - same input always gives same output, no side effects
function add(a, b) {
return a + b;
}
// Impure function - side effects
let total = 0;
function addToTotal(value) {
total += value; // Modifies external state
return total;
}
// Impure function - different output for same input
function getRandom() {
return Math.random();
}
// Pure version of impure functions
function addToTotalPure(total, value) {
return total + value;
}
const newTotal = addToTotalPure(0, 5); // Always returns 5
function getRandomPure(seed) {
// Deterministic pseudo-random
const x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}// Function that returns a function
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Function that takes a function
function applyOperation(arr, operation) {
return arr.map(operation);
}
console.log(applyOperation([1, 2, 3], x => x * 2)); // [2, 4, 6]// Mutable (avoid)
const arr = [1, 2, 3];
arr.push(4); // Mutates original
// Immutable (preferred)
const arr = [1, 2, 3];
const newArr = [...arr, 4]; // Creates new array
// Updating object
const obj = { a: 1, b: 2 };
const newObj = { ...obj, b: 3 }; // Override b
const newObj2 = { ...obj, c: 3 }; // Add c
// Deep clone for nested objects
const deepClone = JSON.parse(JSON.stringify(original));
// Or use a library like Lodash
// import _ from 'lodash';
// const clone = _.cloneDeep(original);
// Immer library for complex immutable updates
// import { produce } from 'immer';
// const nextState = produce(state, draft => {
// draft.users[0].name = 'Alice';
// });const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map - transform each element
const doubled = numbers.map(n => n * 2);
// filter - keep elements that pass test
const evens = numbers.filter(n => n % 2 === 0);
// reduce - combine into single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
// find - get first matching element
const firstEven = numbers.find(n => n % 2 === 0);
// findIndex - get index of first match
const firstEvenIndex = numbers.findIndex(n => n % 2 === 0);
// some - check if any element passes
const hasEven = numbers.some(n => n % 2 === 0);
// every - check if all elements pass
const allPositive = numbers.every(n => n > 0);
// flatMap - map then flatten
const words = ["hello", "world"];
const chars = words.flatMap(word => word.split(""));
// ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]
// Chaining
const result = numbers
.filter(n => n > 3)
.map(n => n * 2)
.reduce((a, b) => a + b, 0);// Basic GET request
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Async/await version
async function getData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
}
}
// POST request
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
}
// Using async/await
const user = await createUser({
name: 'Alice',
email: 'alice@example.com'
});// Parse JSON string to JavaScript object
const jsonString = '{"name": "Alice", "age": 25}';
const obj = JSON.parse(jsonString);
// Convert JavaScript object to JSON string
const json = JSON.stringify(obj, null, 2); // Pretty print
// JSON with reviver function
const data = JSON.parse(jsonString, (key, value) => {
if (key === 'age') {
return value * 2; // Transform value
}
return value;
});
// JSON with replacer
const json = JSON.stringify(obj, ['name', 'age'], 2);
// Only includes name and age properties// RESTful API methods
const api = {
// GET - retrieve data
async get(url) {
const response = await fetch(url);
return response.json();
},
// POST - create new resource
async post(url, data) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
},
// PUT - update entire resource
async put(url, data) {
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
},
// PATCH - partial update
async patch(url, data) {
const response = await fetch(url, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
},
// DELETE - remove resource
async delete(url) {
await fetch(url, { method: 'DELETE' });
}
};
// Using the API
const users = await api.get('/api/users');
const newUser = await api.post('/api/users', { name: 'Alice' });
const updatedUser = await api.patch('/api/users/1', { name: 'Bob' });
await api.delete('/api/users/1');// Simple todo list with localStorage
class TodoList {
constructor() {
this.todos = JSON.parse(localStorage.getItem('todos')) || [];
}
add(todo) {
this.todos.push({
id: Date.now(),
text: todo,
completed: false,
createdAt: new Date().toISOString()
});
this.save();
}
toggle(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.save();
}
}
delete(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.save();
}
save() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
}
// Usage
const todoList = new TodoList();
todoList.add("Learn JavaScript");
todoList.add("Build a project");async function getWeather(city) {
const apiKey = 'YOUR_API_KEY';
const url = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Weather data not available');
}
const data = await response.json();
return {
location: data.location.name,
temperature: data.current.temp_c,
condition: data.current.condition.text,
humidity: data.current.humidity
};
} catch (error) {
console.error('Error fetching weather:', error);
return null;
}
}| Topic | Key Concepts |
|---|---|
| ES6+ Features | Arrow functions, destructuring, spread, modules |
| Async JavaScript | Promises, async/await, fetch API |
| DOM Manipulation | Select, modify, create elements, events |
| Error Handling | Try/catch, throwing errors, custom errors |
| Functional Programming | Pure functions, immutability, array methods |
| APIs | REST concepts, fetch, JSON |
- Move to the Advanced Guide for deeper topics
- Practice building projects with the concepts learned
- Explore DOM Manipulation in detail
- Learn about Testing your code
// Arrow functions
const add = (a, b) => a + b;
// Destructuring
const { name, age } = person;
const [first, ...rest] = arr;
// Spread
const newArr = [...arr1, ...arr2];
const newObj = { ...obj1, ...obj2 };
// Async/await
async function fetchData() {
try {
const data = await fetch(url);
return await data.json();
} catch (error) {
console.error(error);
}
}
// DOM
document.querySelector('.class');
element.addEventListener('click', handler);
element.textContent = 'new text';