JavaScript's classic role is to manipulate the Document Object Model (DOM). You can select a single element with document.getElementById('id') or document.querySelector('.class'), and you can select many with document.querySelectorAll(), which returns a NodeList. Once you have an element, you can attach an event listener such as element.addEventListener('click', handler) and later remove it with removeEventListener(). The handler receives an event object e with details like e.target and helpers such as e.preventDefault() to cancel the browser's default behavior.
Instead of attaching listeners to many child elements, event delegation attaches a single listener to a parent and uses the fact that events bubble up through the DOM tree. This pattern improves performance, especially with hundreds of nodes, and works well for dynamically added elements. A common mistake is to add a separate listener to each child when one parent listener would handle them all. As with other techniques, trying it on a small realistic example and articulating the decision out loud—"what problem is this solving?", "what trade-off does it create?", and "how will I know it worked?"—is the best way to internalize the pattern before applying it broadly.
Modern JavaScript includes several conveniences and patterns worth knowing. Optional chaining (?.) safely reads nested properties or calls methods without throwing when an intermediate value is null or undefined. The nullish coalescing operator (??) returns its right-hand side only when the left side is null or undefined, unlike ||, which also treats values like 0, '', and false as falsy fallbacks. A pure function always returns the same output for the same input and avoids side effects such as mutating external state or making network calls, which makes programs easier to test and reason about. Debouncing delays a function call until activity stops, commonly used so that a search input triggers an API call only after the user pauses typing. Throttling ensures a function runs at most once per defined interval, ideal for handlers such as scroll, resize, and mousemove. Finally, embracing immutability—creating new arrays and objects rather than mutating existing ones—makes state changes predictable in UI frameworks and reducers; the common mistake is to mutate arrays or objects directly and expect change detection to work anyway. Together, these tools compose into idiomatic, robust JavaScript.