JavaScript Best Practices & Performance Optimization
JavaScript Best Practices & Performance Optimization
Welcome to the final lesson of our JavaScript Essentials course! In this comprehensive lesson, we'll explore best practices, performance optimization techniques, security considerations, and strategies for writing maintainable, efficient JavaScript code. These practices will help you become a professional JavaScript developer.
1. Code Organization and Structure
File Organization
Organize your code into logical modules and files:
<!-- Bad: Everything in one file -->
<script>
// 5000 lines of mixed code...
</script>
<!-- Good: Organized structure -->
project/
├── src/
│ ├── components/
│ │ ├── header.js
│ │ └── footer.js
│ ├── utils/
│ │ ├── validation.js
│ │ └── formatting.js
│ ├── services/
│ │ └── api.js
│ └── app.js
└── index.html
Module Pattern
Use ES6 modules to organize code:
// utils/validation.js
export function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
export function validatePhone(phone) {
const re = /^\+?[1-9]\d{1,14}$/;
return re.test(phone);
}
// app.js
import { validateEmail, validatePhone } from './utils/validation.js';
const email = 'user@example.com';
console.log(validateEmail(email)); // true
Naming Conventions
Follow consistent naming conventions:
// Variables and functions: camelCase
let userName = 'John';
function getUserData() { }
// Constants: UPPER_SNAKE_CASE
const MAX_RETRY_ATTEMPTS = 3;
const API_BASE_URL = 'https://api.example.com';
// Classes: PascalCase
class UserProfile { }
class ShoppingCart { }
// Private properties: prefix with underscore
class BankAccount {
constructor() {
this._balance = 0; // Private convention
}
}
// Boolean variables: use is/has/can prefix
let isActive = true;
let hasPermission = false;
let canEdit = true;
2. Variable Best Practices
Prefer const, Use let, Avoid var
Always use const by default, only use let when you need reassignment:
// Bad: Using var
var count = 0;
var name = 'John';
// Good: Use const by default
const name = 'John';
const MAX_ITEMS = 100;
const config = { theme: 'dark' };
// Good: Use let when reassignment needed
let count = 0;
count++;
let status = 'pending';
status = 'completed';
Meaningful Variable Names
Use descriptive names that reveal intent:
// Bad: Unclear names
let d = new Date();
let x = users.filter(u => u.a);
let temp = calculate();
// Good: Descriptive names
let currentDate = new Date();
let activeUsers = users.filter(user => user.isActive);
let totalPrice = calculateTotalPrice();
// Bad: Single letter (except loops)
let a = 5;
let b = 10;
// Good: Descriptive
let width = 5;
let height = 10;
// Acceptable: Common loop variables
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
Avoid Global Variables
Minimize global scope pollution:
// Bad: Global variables
var userData = {};
var isLoggedIn = false;
function login() {
isLoggedIn = true;
}
// Good: Use modules or IIFE
const App = (function() {
let userData = {};
let isLoggedIn = false;
function login() {
isLoggedIn = true;
}
return {
login
};
})();
// Better: ES6 modules
// auth.js
let isLoggedIn = false;
export function login() {
isLoggedIn = true;
}
export function isAuthenticated() {
return isLoggedIn;
}
3. Function Best Practices
Single Responsibility Principle
Each function should do one thing well:
// Bad: Function doing too much
function processUserData(user) {
// Validate
if (!user.email) return false;
// Transform
user.name = user.name.toUpperCase();
// Save to database
database.save(user);
// Send email
emailService.send(user.email);
// Log
console.log('User processed');
}
// Good: Separate concerns
function validateUser(user) {
return user.email !== undefined;
}
function normalizeUserData(user) {
return {
...user,
name: user.name.toUpperCase()
};
}
function saveUser(user) {
return database.save(user);
}
function sendWelcomeEmail(user) {
return emailService.send(user.email);
}
function processUser(user) {
if (!validateUser(user)) {
throw new Error('Invalid user data');
}
const normalized = normalizeUserData(user);
const saved = saveUser(normalized);
sendWelcomeEmail(saved);
console.log('User processed successfully');
return saved;
}
Pure Functions
Write pure functions when possible (no side effects, same input = same output):
// Impure: Modifies external state
let total = 0;
function addToTotal(value) {
total += value; // Side effect
return total;
}
// Pure: No side effects
function add(a, b) {
return a + b;
}
// Impure: Modifies input
function addItem(cart, item) {
cart.items.push(item); // Mutates input
return cart;
}
// Pure: Returns new object
function addItem(cart, item) {
return {
...cart,
items: [...cart.items, item]
};
}
Function Size and Complexity
Keep functions small and focused:
// Bad: Long, complex function
function calculateOrderTotal(order) {
let subtotal = 0;
for (let item of order.items) {
subtotal += item.price * item.quantity;
}
let discount = 0;
if (order.couponCode) {
if (order.couponCode === 'SAVE10') {
discount = subtotal * 0.1;
} else if (order.couponCode === 'SAVE20') {
discount = subtotal * 0.2;
}
}
let tax = 0;
if (order.country === 'US') {
tax = (subtotal - discount) * 0.08;
} else if (order.country === 'UK') {
tax = (subtotal - discount) * 0.20;
}
return subtotal - discount + tax;
}
// Good: Broken into smaller functions
function calculateSubtotal(items) {
return items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0);
}
function calculateDiscount(subtotal, couponCode) {
const discounts = {
'SAVE10': 0.1,
'SAVE20': 0.2
};
return subtotal * (discounts[couponCode] || 0);
}
function calculateTax(amount, country) {
const taxRates = {
'US': 0.08,
'UK': 0.20
};
return amount * (taxRates[country] || 0);
}
function calculateOrderTotal(order) {
const subtotal = calculateSubtotal(order.items);
const discount = calculateDiscount(subtotal, order.couponCode);
const taxableAmount = subtotal - discount;
const tax = calculateTax(taxableAmount, order.country);
return taxableAmount + tax;
}
4. Error Handling Strategy
Consistent Error Handling
Use try-catch blocks appropriately:
// Bad: Silent failures
function loadUserData(userId) {
try {
const data = localStorage.getItem(userId);
return JSON.parse(data);
} catch (e) {
// Silent failure - bad!
}
}
// Good: Proper error handling
function loadUserData(userId) {
try {
const data = localStorage.getItem(userId);
if (!data) {
throw new Error('User data not found');
}
return JSON.parse(data);
} catch (error) {
console.error('Failed to load user data:', error);
throw error; // Re-throw or handle appropriately
}
}
// Async error handling
async function fetchUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching user profile:', error);
throw error;
}
}
Custom Error Classes
Create custom errors for better error handling:
// Define custom error classes
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
// Use custom errors
function validateEmail(email) {
if (!email) {
throw new ValidationError('Email is required', 'email');
}
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
return true;
}
// Handle specific error types
try {
validateEmail('');
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed for ${error.field}: ${error.message}`);
} else {
console.error('Unexpected error:', error);
}
}
5. Performance Optimization
DOM Manipulation Best Practices
Minimize DOM access and batch updates:
// Bad: Multiple DOM manipulations
function addItems(items) {
const list = document.getElementById('list');
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
list.appendChild(li); // Triggers reflow each time
});
}
// Good: Batch DOM updates
function addItems(items) {
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
list.appendChild(fragment); // Single reflow
}
// Better: Use innerHTML for large lists
function addItems(items) {
const list = document.getElementById('list');
const html = items.map(item => `<li>${item}</li>`).join('');
list.innerHTML = html;
}
Event Delegation
Use event delegation for better performance:
// Bad: Adding listeners to each item
function attachListeners() {
const buttons = document.querySelectorAll('.item-button');
buttons.forEach(button => {
button.addEventListener('click', handleClick);
});
}
// Good: Event delegation
function attachListeners() {
const container = document.getElementById('items-container');
container.addEventListener('click', (e) => {
if (e.target.classList.contains('item-button')) {
handleClick(e);
}
});
}
// Works for dynamically added elements too
function addNewItem() {
const container = document.getElementById('items-container');
const button = document.createElement('button');
button.className = 'item-button';
button.textContent = 'New Item';
container.appendChild(button);
// No need to attach listener - delegation handles it!
}
Debouncing and Throttling
Control function execution frequency:
// Debounce: Execute after delay since last call
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: Search as user types
const searchInput = document.getElementById('search');
const performSearch = debounce((query) => {
console.log('Searching for:', query);
// API call here
}, 300);
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
// Throttle: Execute at most once per interval
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage: Scroll event
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);
Avoid Memory Leaks
Clean up event listeners and references:
// Bad: Memory leak
function attachListener() {
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
// This creates a closure that may prevent garbage collection
const largeData = new Array(1000000);
console.log(largeData);
});
}
// Good: Clean up
class Component {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('Clicked');
}
destroy() {
// Remove event listener
this.element.removeEventListener('click', this.handleClick);
// Clear references
this.element = null;
this.handleClick = null;
}
}
// Usage
const component = new Component(document.getElementById('myButton'));
// Later, when component is no longer needed
component.destroy();
RequestAnimationFrame for Animations
Use requestAnimationFrame for smooth animations:
// Bad: Using setTimeout
function animate() {
const element = document.getElementById('box');
let position = 0;
setInterval(() => {
position += 5;
element.style.left = position + 'px';
}, 16); // ~60fps
}
// Good: Using requestAnimationFrame
function animate() {
const element = document.getElementById('box');
let position = 0;
function step() {
position += 5;
element.style.left = position + 'px';
if (position < 500) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
// Better: With timing control
function animate() {
const element = document.getElementById('box');
let start = null;
const duration = 2000; // 2 seconds
const distance = 500;
function step(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
const percentage = Math.min(progress / duration, 1);
element.style.left = (distance * percentage) + 'px';
if (progress < duration) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
6. Security Best Practices
XSS Prevention
Always sanitize user input:
// Bad: Direct insertion of user input
function displayMessage(message) {
document.getElementById('output').innerHTML = message;
// Vulnerable to XSS: <script>alert('hacked')</script>
}
// Good: Use textContent
function displayMessage(message) {
document.getElementById('output').textContent = message;
// Script tags rendered as text, not executed
}
// Good: Sanitize HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function displayMessage(message) {
const sanitized = escapeHtml(message);
document.getElementById('output').innerHTML = sanitized;
}
// Better: Use a sanitization library
import DOMPurify from 'dompurify';
function displayMessage(message) {
const clean = DOMPurify.sanitize(message);
document.getElementById('output').innerHTML = clean;
}
Input Validation
Always validate and sanitize input:
// Input validation utilities
const Validator = {
isEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
},
isUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
},
isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
},
isInRange(value, min, max) {
const num = Number(value);
return num >= min && num <= max;
},
hasMinLength(str, length) {
return str.length >= length;
}
};
// Usage
function validateForm(data) {
const errors = {};
if (!Validator.isEmail(data.email)) {
errors.email = 'Invalid email address';
}
if (!Validator.hasMinLength(data.password, 8)) {
errors.password = 'Password must be at least 8 characters';
}
if (!Validator.isInRange(data.age, 18, 120)) {
errors.age = 'Age must be between 18 and 120';
}
return {
isValid: Object.keys(errors).length === 0,
errors
};
}
Content Security Policy
Implement CSP headers:
<!-- Add CSP meta tag -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;">
<!-- Or set in HTTP headers (preferred) -->
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com
<!-- Avoid inline scripts -->
<!-- Bad -->
<button onclick="doSomething()">Click</button>
<!-- Good -->
<button id="myButton">Click</button>
<script src="app.js"></script>
// app.js
document.getElementById('myButton').addEventListener('click', doSomething);
7. Accessibility in JavaScript
ARIA Attributes
Add ARIA attributes for screen readers:
// Managing ARIA attributes
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
// Add ARIA attributes
notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'polite');
notification.setAttribute('aria-atomic', 'true');
document.body.appendChild(notification);
// Remove after delay
setTimeout(() => {
notification.remove();
}, 5000);
}
// Toggle button state
function toggleButton(button) {
const isPressed = button.getAttribute('aria-pressed') === 'true';
button.setAttribute('aria-pressed', !isPressed);
button.textContent = isPressed ? 'Show' : 'Hide';
}
// Expandable section
function toggleSection(button, sectionId) {
const section = document.getElementById(sectionId);
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
section.hidden = isExpanded;
}
Focus Management
Manage focus for better keyboard navigation:
// Modal focus trap
class Modal {
constructor(element) {
this.element = element;
this.focusableElements = null;
this.firstFocusable = null;
this.lastFocusable = null;
this.previousFocus = null;
}
open() {
// Save current focus
this.previousFocus = document.activeElement;
// Show modal
this.element.style.display = 'block';
this.element.setAttribute('aria-hidden', 'false');
// Get focusable elements
this.focusableElements = this.element.querySelectorAll(
'a[href], button:not([disabled]), textarea, input, select'
);
this.firstFocusable = this.focusableElements[0];
this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
// Focus first element
this.firstFocusable.focus();
// Add event listeners
this.element.addEventListener('keydown', this.handleKeydown.bind(this));
}
close() {
// Hide modal
this.element.style.display = 'none';
this.element.setAttribute('aria-hidden', 'true');
// Restore focus
if (this.previousFocus) {
this.previousFocus.focus();
}
// Remove event listeners
this.element.removeEventListener('keydown', this.handleKeydown);
}
handleKeydown(e) {
// Close on Escape
if (e.key === 'Escape') {
this.close();
return;
}
// Trap focus with Tab
if (e.key === 'Tab') {
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable.focus();
}
} else {
// Tab
if (document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable.focus();
}
}
}
}
}
Keyboard Navigation
Implement keyboard support:
// Custom dropdown with keyboard support
class Dropdown {
constructor(button, menu) {
this.button = button;
this.menu = menu;
this.items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
this.currentIndex = -1;
this.button.addEventListener('click', () => this.toggle());
this.button.addEventListener('keydown', (e) => this.handleButtonKeydown(e));
this.menu.addEventListener('keydown', (e) => this.handleMenuKeydown(e));
}
toggle() {
const isOpen = this.menu.getAttribute('aria-hidden') === 'false';
if (isOpen) {
this.close();
} else {
this.open();
}
}
open() {
this.menu.setAttribute('aria-hidden', 'false');
this.button.setAttribute('aria-expanded', 'true');
this.items[0].focus();
this.currentIndex = 0;
}
close() {
this.menu.setAttribute('aria-hidden', 'true');
this.button.setAttribute('aria-expanded', 'false');
this.button.focus();
this.currentIndex = -1;
}
handleButtonKeydown(e) {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault();
this.open();
}
}
handleMenuKeydown(e) {
switch(e.key) {
case 'Escape':
e.preventDefault();
this.close();
break;
case 'ArrowDown':
e.preventDefault();
this.currentIndex = (this.currentIndex + 1) % this.items.length;
this.items[this.currentIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
this.currentIndex = this.currentIndex - 1;
if (this.currentIndex < 0) {
this.currentIndex = this.items.length - 1;
}
this.items[this.currentIndex].focus();
break;
case 'Home':
e.preventDefault();
this.currentIndex = 0;
this.items[0].focus();
break;
case 'End':
e.preventDefault();
this.currentIndex = this.items.length - 1;
this.items[this.currentIndex].focus();
break;
case 'Enter':
case ' ':
e.preventDefault();
this.items[this.currentIndex].click();
this.close();
break;
}
}
}
8. Testing Introduction
Unit Testing Basics
Write testable code and basic tests:
// Testable function
function calculateDiscount(price, discountPercent) {
if (price < 0 || discountPercent < 0 || discountPercent > 100) {
throw new Error('Invalid input');
}
return price * (discountPercent / 100);
}
// Simple test framework
function test(description, testFn) {
try {
testFn();
console.log(`✓ ${description}`);
} catch (error) {
console.error(`✗ ${description}`);
console.error(error.message);
}
}
function assertEquals(actual, expected) {
if (actual !== expected) {
throw new Error(`Expected ${expected} but got ${actual}`);
}
}
function assertThrows(fn, errorMessage) {
try {
fn();
throw new Error('Expected function to throw');
} catch (error) {
if (!error.message.includes(errorMessage)) {
throw new Error(`Expected error message to include "${errorMessage}"`);
}
}
}
// Run tests
test('calculates 10% discount correctly', () => {
const result = calculateDiscount(100, 10);
assertEquals(result, 10);
});
test('calculates 50% discount correctly', () => {
const result = calculateDiscount(200, 50);
assertEquals(result, 100);
});
test('throws error for negative price', () => {
assertThrows(() => calculateDiscount(-100, 10), 'Invalid input');
});
test('throws error for invalid discount', () => {
assertThrows(() => calculateDiscount(100, 150), 'Invalid input');
});
Test Structure
Organize tests with arrange-act-assert pattern:
// User class to test
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.isActive = false;
}
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
updateEmail(newEmail) {
if (!newEmail.includes('@')) {
throw new Error('Invalid email');
}
this.email = newEmail;
}
}
// Tests using AAA pattern
test('User activation works correctly', () => {
// Arrange
const user = new User('John Doe', 'john@example.com');
// Act
user.activate();
// Assert
assertEquals(user.isActive, true);
});
test('User deactivation works correctly', () => {
// Arrange
const user = new User('Jane Doe', 'jane@example.com');
user.activate();
// Act
user.deactivate();
// Assert
assertEquals(user.isActive, false);
});
test('Email update validates format', () => {
// Arrange
const user = new User('Bob Smith', 'bob@example.com');
// Act & Assert
assertThrows(
() => user.updateEmail('invalid-email'),
'Invalid email'
);
});
test('Email update works with valid email', () => {
// Arrange
const user = new User('Alice Johnson', 'alice@example.com');
const newEmail = 'alice.new@example.com';
// Act
user.updateEmail(newEmail);
// Assert
assertEquals(user.email, newEmail);
});
9. Code Review Checklist
Before Submitting Code
Review your code against this checklist:
// Code Quality Checklist
const codeReviewChecklist = {
// Functionality
functionality: [
'Does the code work as intended?',
'Are edge cases handled?',
'Are error conditions handled?',
'Is input validated?'
],
// Code Style
style: [
'Are naming conventions followed?',
'Is indentation consistent?',
'Are comments clear and helpful?',
'Is code properly formatted?'
],
// Best Practices
bestPractices: [
'Are functions small and focused?',
'Is code DRY (Don't Repeat Yourself)?',
'Are constants used for magic numbers?',
'Is const used by default?'
],
// Performance
performance: [
'Are DOM manipulations batched?',
'Are event listeners cleaned up?',
'Are expensive operations cached?',
'Is debouncing/throttling used where appropriate?'
],
// Security
security: [
'Is user input sanitized?',
'Are XSS vulnerabilities prevented?',
'Is sensitive data protected?',
'Are API keys kept secure?'
],
// Accessibility
accessibility: [
'Are ARIA attributes used correctly?',
'Is keyboard navigation supported?',
'Are focus states visible?',
'Are color contrasts sufficient?'
],
// Testing
testing: [
'Are tests written and passing?',
'Is test coverage adequate?',
'Are edge cases tested?',
'Are error conditions tested?'
],
// Documentation
documentation: [
'Are complex sections commented?',
'Is API documentation updated?',
'Are README instructions clear?',
'Are breaking changes noted?'
]
};
10. Staying Current with JavaScript
TC39 Process
Understanding how JavaScript evolves:
// TC39 Proposal Stages:
// Stage 0: Strawperson - Just an idea
// Stage 1: Proposal - Worth pursuing
// Stage 2: Draft - Precise syntax
// Stage 3: Candidate - Complete, awaiting implementation feedback
// Stage 4: Finished - Will be in next ECMAScript release
// Example: Recent additions
// Stage 4 (finalized):
// - Optional Chaining (?.)
const user = { profile: { name: 'John' } };
console.log(user?.profile?.name); // 'John'
console.log(user?.settings?.theme); // undefined (no error)
// - Nullish Coalescing (??)
const value = null ?? 'default'; // 'default'
const zero = 0 ?? 'default'; // 0 (not 'default')
// - Private Fields (#)
class BankAccount {
#balance = 0; // Private field
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
// - Top-level await
// In modules:
const data = await fetch('/api/data').then(r => r.json());
// - Promise.allSettled()
const results = await Promise.allSettled([
fetch('/api/1'),
fetch('/api/2'),
fetch('/api/3')
]);
// Returns all results, even if some fail
// Resources to stay updated:
// - https://github.com/tc39/proposals
// - https://developer.mozilla.org/en-US/
// - JavaScript Weekly newsletter
// - Twitter: @TC39
// - Can I Use (caniuse.com) for browser support
Comprehensive Best Practices Example
Here's a complete example following all best practices:
// taskManager.js - Complete example following best practices
// Constants
const TASK_STATUS = {
PENDING: 'pending',
IN_PROGRESS: 'in_progress',
COMPLETED: 'completed'
};
const MAX_TASKS = 100;
// Custom error
class TaskError extends Error {
constructor(message, code) {
super(message);
this.name = 'TaskError';
this.code = code;
}
}
// Validation utilities
const Validator = {
isValidTitle(title) {
return typeof title === 'string' && title.trim().length > 0;
},
isValidStatus(status) {
return Object.values(TASK_STATUS).includes(status);
}
};
// Task class with private fields
class Task {
#id;
#title;
#status;
#createdAt;
constructor(title) {
if (!Validator.isValidTitle(title)) {
throw new TaskError('Invalid task title', 'INVALID_TITLE');
}
this.#id = crypto.randomUUID();
this.#title = title;
this.#status = TASK_STATUS.PENDING;
this.#createdAt = new Date();
}
// Getters
get id() { return this.#id; }
get title() { return this.#title; }
get status() { return this.#status; }
get createdAt() { return this.#createdAt; }
// Methods
updateStatus(newStatus) {
if (!Validator.isValidStatus(newStatus)) {
throw new TaskError('Invalid status', 'INVALID_STATUS');
}
this.#status = newStatus;
}
toJSON() {
return {
id: this.#id,
title: this.#title,
status: this.#status,
createdAt: this.#createdAt.toISOString()
};
}
}
// Task Manager class
class TaskManager {
#tasks;
#listeners;
constructor() {
this.#tasks = new Map();
this.#listeners = new Set();
this.#loadFromStorage();
}
// Public methods
addTask(title) {
if (this.#tasks.size >= MAX_TASKS) {
throw new TaskError('Maximum tasks reached', 'MAX_TASKS');
}
try {
const task = new Task(title);
this.#tasks.set(task.id, task);
this.#saveToStorage();
this.#notifyListeners('taskAdded', task);
return task;
} catch (error) {
console.error('Failed to add task:', error);
throw error;
}
}
removeTask(taskId) {
const task = this.#tasks.get(taskId);
if (!task) {
throw new TaskError('Task not found', 'NOT_FOUND');
}
this.#tasks.delete(taskId);
this.#saveToStorage();
this.#notifyListeners('taskRemoved', task);
}
updateTaskStatus(taskId, status) {
const task = this.#tasks.get(taskId);
if (!task) {
throw new TaskError('Task not found', 'NOT_FOUND');
}
task.updateStatus(status);
this.#saveToStorage();
this.#notifyListeners('taskUpdated', task);
}
getTasks() {
return Array.from(this.#tasks.values());
}
getTasksByStatus(status) {
return this.getTasks().filter(task => task.status === status);
}
// Event system
on(event, callback) {
this.#listeners.add({ event, callback });
}
off(event, callback) {
this.#listeners.forEach(listener => {
if (listener.event === event && listener.callback === callback) {
this.#listeners.delete(listener);
}
});
}
// Private methods
#notifyListeners(event, data) {
this.#listeners.forEach(listener => {
if (listener.event === event) {
listener.callback(data);
}
});
}
#saveToStorage() {
try {
const data = {
tasks: Array.from(this.#tasks.values()).map(task => task.toJSON())
};
localStorage.setItem('taskManager', JSON.stringify(data));
} catch (error) {
console.error('Failed to save to storage:', error);
}
}
#loadFromStorage() {
try {
const data = localStorage.getItem('taskManager');
if (data) {
const parsed = JSON.parse(data);
parsed.tasks.forEach(taskData => {
const task = new Task(taskData.title);
task.updateStatus(taskData.status);
this.#tasks.set(task.id, task);
});
}
} catch (error) {
console.error('Failed to load from storage:', error);
}
}
}
// UI Controller with accessibility
class TaskUI {
constructor(taskManager, container) {
this.taskManager = taskManager;
this.container = container;
this.#setupEventListeners();
this.#render();
}
#setupEventListeners() {
// Event delegation for task list
this.container.addEventListener('click', this.#handleClick.bind(this));
// Listen to task manager events
this.taskManager.on('taskAdded', () => this.#render());
this.taskManager.on('taskRemoved', () => this.#render());
this.taskManager.on('taskUpdated', () => this.#render());
}
#handleClick(e) {
const target = e.target;
if (target.classList.contains('task-complete')) {
const taskId = target.dataset.taskId;
this.taskManager.updateTaskStatus(taskId, TASK_STATUS.COMPLETED);
} else if (target.classList.contains('task-delete')) {
const taskId = target.dataset.taskId;
this.taskManager.removeTask(taskId);
}
}
#render() {
const tasks = this.taskManager.getTasks();
// Use DocumentFragment for better performance
const fragment = document.createDocumentFragment();
tasks.forEach(task => {
const item = this.#createTaskElement(task);
fragment.appendChild(item);
});
// Clear and append
this.container.innerHTML = '';
this.container.appendChild(fragment);
// Announce to screen readers
this.#announceTaskCount(tasks.length);
}
#createTaskElement(task) {
const li = document.createElement('li');
li.className = `task-item task-${task.status}`;
li.setAttribute('role', 'listitem');
const checkbox = document.createElement('button');
checkbox.className = 'task-complete';
checkbox.dataset.taskId = task.id;
checkbox.setAttribute('aria-label', `Mark ${task.title} as complete`);
checkbox.textContent = task.status === TASK_STATUS.COMPLETED ? '✓' : '○';
const title = document.createElement('span');
title.className = 'task-title';
title.textContent = task.title;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'task-delete';
deleteBtn.dataset.taskId = task.id;
deleteBtn.setAttribute('aria-label', `Delete ${task.title}`);
deleteBtn.textContent = '×';
li.appendChild(checkbox);
li.appendChild(title);
li.appendChild(deleteBtn);
return li;
}
#announceTaskCount(count) {
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', 'polite');
announcement.className = 'sr-only';
announcement.textContent = `${count} tasks in list`;
document.body.appendChild(announcement);
setTimeout(() => announcement.remove(), 1000);
}
}
// Initialize application
function initializeApp() {
try {
const taskManager = new TaskManager();
const container = document.getElementById('task-list');
const taskUI = new TaskUI(taskManager, container);
// Add task form handler
const form = document.getElementById('add-task-form');
const input = document.getElementById('task-input');
form.addEventListener('submit', (e) => {
e.preventDefault();
const title = input.value.trim();
if (title) {
try {
taskManager.addTask(title);
input.value = '';
} catch (error) {
if (error instanceof TaskError) {
alert(error.message);
}
}
}
});
// Expose for debugging (remove in production)
window.taskManager = taskManager;
} catch (error) {
console.error('Failed to initialize app:', error);
}
}
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}
export { TaskManager, Task, TASK_STATUS };
Final Project Exercise
Create a complete application following all best practices:
- Build a notes application with categories
- Implement CRUD operations (Create, Read, Update, Delete)
- Add search and filter functionality
- Implement localStorage persistence
- Add keyboard shortcuts (Ctrl+N for new note, etc.)
- Make it fully accessible (ARIA, keyboard navigation)
- Optimize performance (debounce search, batch DOM updates)
- Add input validation and error handling
- Write unit tests for core functions
- Document your code with comments
Summary
Congratulations on completing the JavaScript Essentials course! You've learned:
- Code Organization: Proper file structure, modules, and naming conventions
- Variable Best Practices: Using const/let, meaningful names, avoiding globals
- Function Best Practices: Single responsibility, pure functions, small focused functions
- Error Handling: Consistent patterns, custom errors, proper logging
- Performance Optimization: DOM manipulation, event delegation, debouncing, throttling
- Security: XSS prevention, input validation, CSP
- Accessibility: ARIA attributes, focus management, keyboard navigation
- Testing: Unit test basics, test structure, assertions
- Code Review: Quality checklist, review process
- Staying Current: TC39 process, resources for learning
Keep coding, keep learning, and always strive to write clean, maintainable, and efficient JavaScript code!