DOM Manipulation & Browser APIs

· Seokhyeon Byun

Note: This post is based on my old programming study notes when I taught myself.

Script Loading Strategies

Understanding how to efficiently load JavaScript in HTML is crucial for web performance.

Async vs Defer

Both async and defer attributes allow scripts to be downloaded in parallel with HTML parsing, but they execute at different times.

<!-- Regular script (blocking) -->
<script src="script.js"></script>

<!-- Async script (executes immediately when downloaded) -->
<script async src="script.js"></script>

<!-- Defer script (executes after HTML parsing) -->
<script defer src="script.js"></script>

Loading Comparison

Case 1: <script> in <head> (Blocking)

  • Browser pauses HTML parsing to download and execute JavaScript
  • Disadvantage: Users see blank page until JavaScript loads
  • Use case: Critical scripts needed before page renders

Case 2: <script> at end of <body>

  • JavaScript loads after HTML parsing is complete
  • Advantage: Users see content quickly
  • Disadvantage: JavaScript-dependent features unavailable initially

Case 3: <script async> in <head>

  • Downloads script while parsing HTML, executes immediately when ready
  • Advantage: Faster than blocking scripts
  • Disadvantage: Can interrupt HTML parsing during execution

Case 4: <script defer> in <head> (Recommended)

  • Downloads script while parsing HTML, executes after parsing completes
  • Advantage: Best of both worlds - fast loading and proper execution order
  • Use case: Most external scripts
// Example: Multiple defer scripts execute in order
// script1.js executes before script2.js
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>

Interview Question: What’s the difference between async and defer?

Answer: async executes scripts immediately when downloaded (potentially blocking HTML parsing), while defer waits until HTML parsing is complete and maintains execution order.


DOM Fundamentals

Window Object

The global window object represents the browser window and serves as the global scope in browsers.

// Window properties and methods
console.log(window.innerWidth); // Browser width
console.log(window.innerHeight); // Browser height
console.log(window.location.href); // Current URL

// Global variables become window properties
var globalVar = "I'm global";
console.log(window.globalVar); // "I'm global"

// Window methods
window.alert("Hello World");
window.confirm("Are you sure?");
window.prompt("Enter your name:");

// Navigation
window.open("https://example.com");
window.close(); // Close current window
window.location.reload(); // Refresh page

Document Object

The document object represents the loaded HTML document and is the entry point to the DOM tree.

// Document properties
console.log(document.title); // Page title
console.log(document.URL); // Current URL
console.log(document.domain); // Domain name

// Document structure
console.log(document.documentElement); // <html> element
console.log(document.head); // <head> element
console.log(document.body); // <body> element

// Document state
console.log(document.readyState); // "loading", "interactive", or "complete"

// Create elements
const newDiv = document.createElement("div");
const textNode = document.createTextNode("Hello World");

Element Selection

ID and Class Selectors

// ID selector (returns single element or null)
const header = document.getElementById("main-header");
console.log(header); // Element or null

// Class selectors (returns HTMLCollection)
const buttons = document.getElementsByClassName("btn");
const paragraphs = document.getElementsByTagName("p");

// Convert HTMLCollection to Array
const buttonArray = Array.from(buttons);
const paragraphArray = [...paragraphs];

// Example usage
if (header) {
  header.style.color = "blue";
}

// Iterate through collections
for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", handleClick);
}

Query Selectors

Modern, flexible selection methods using CSS selector syntax.

// querySelector (returns first match or null)
const firstButton = document.querySelector(".btn");
const navLink = document.querySelector('nav a[href="/home"]');
const firstChild = document.querySelector("ul > li:first-child");

// querySelectorAll (returns NodeList)
const allButtons = document.querySelectorAll(".btn");
const allLinks = document.querySelectorAll("a");
const specificInputs = document.querySelectorAll('input[type="email"]');

// NodeList can use forEach directly
allButtons.forEach((button) => {
  button.addEventListener("click", handleClick);
});

// Advanced selectors
const complexSelector = document.querySelectorAll(
  ".container .item:nth-child(odd):not(.disabled)"
);

// Selector performance comparison
// ID selector (fastest)
document.getElementById("myId");

// Query selector (more flexible, slightly slower)
document.querySelector("#myId");

Interview Question: What’s the difference between getElementsByClassName and querySelectorAll?

Answer: getElementsByClassName returns a live HTMLCollection that updates automatically when the DOM changes, while querySelectorAll returns a static NodeList snapshot.


Event Handling

Event Listeners

Modern event handling using addEventListener provides better control and flexibility.

// Basic event listener
const button = document.querySelector("#submit-btn");

button.addEventListener("click", function (event) {
  console.log("Button clicked!");
  event.preventDefault(); // Prevent default behavior
});

// Arrow function listener
button.addEventListener("click", (e) => {
  console.log("Arrow function handler");
});

// Named function for reusability
function handleSubmit(event) {
  event.preventDefault();
  const form = event.target.closest("form");
  // Handle form submission
}

button.addEventListener("click", handleSubmit);

// Remove event listener
button.removeEventListener("click", handleSubmit);

// Event listener options
button.addEventListener("click", handleSubmit, {
  once: true, // Execute only once
  passive: true, // Never calls preventDefault()
  capture: true, // Capture phase instead of bubble
});

// Multiple event types
const input = document.querySelector("#email");
["focus", "blur", "input"].forEach((eventType) => {
  input.addEventListener(eventType, handleInputEvent);
});

Arrow Functions vs Regular Functions in Event Handlers

const obj = {
  name: "MyObject",

  // Regular function - 'this' refers to the object
  regularHandler: function (event) {
    console.log(this.name); // "MyObject"
    console.log(this === obj); // true
  },

  // Arrow function - 'this' refers to outer scope
  arrowHandler: (event) => {
    console.log(this.name); // undefined (or global context)
    console.log(this === obj); // false
  },

  init: function () {
    const button = document.querySelector("#btn");

    // Use regular function when you need 'this' context
    button.addEventListener("click", this.regularHandler.bind(this));

    // Arrow function preserves outer 'this'
    button.addEventListener("click", (e) => {
      console.log(this.name); // "MyObject" - preserved from outer scope
    });
  },
};

Key Differences:

  • Regular functions: this depends on how they’re called
  • Arrow functions: this is lexically bound (inherited from enclosing scope)
  • Event handlers: Regular functions have this pointing to the element, arrows preserve outer this

Advanced DOM Concepts

Data Attributes

HTML5 data attributes provide a way to store custom data in HTML elements.

<!-- HTML with data attributes -->
<div
  id="user-card"
  data-user-id="123"
  data-user-role="admin"
  data-last-login="2025-01-20"
>
  User Information
</div>
const userCard = document.querySelector("#user-card");

// Reading data attributes
console.log(userCard.dataset.userId); // "123"
console.log(userCard.dataset.userRole); // "admin"
console.log(userCard.dataset.lastLogin); // "2025-01-20"

// Setting data attributes
userCard.dataset.status = "active";
userCard.dataset.loginCount = "5";

// Alternative methods
console.log(userCard.getAttribute("data-user-id")); // "123"
userCard.setAttribute("data-theme", "dark");

// Using data attributes for configuration
function initializeWidget(element) {
  const config = {
    apiUrl: element.dataset.apiUrl,
    timeout: parseInt(element.dataset.timeout) || 5000,
    debug: element.dataset.debug === "true",
  };

  // Initialize with configuration
}

// CSS styling with data attributes
// CSS: [data-status="active"] { color: green; }

DOM Traversal

Navigate between DOM elements using relationship properties.

const element = document.querySelector(".current");

// Parent traversal
console.log(element.parentNode); // Direct parent (including text nodes)
console.log(element.parentElement); // Parent element (excludes text nodes)
console.log(element.closest(".container")); // Nearest ancestor matching selector

// Child traversal
console.log(element.childNodes); // All child nodes (including text)
console.log(element.children); // Only element children
console.log(element.firstChild); // First child node
console.log(element.firstElementChild); // First element child
console.log(element.lastChild); // Last child node
console.log(element.lastElementChild); // Last element child

// Sibling traversal
console.log(element.nextSibling); // Next sibling node
console.log(element.nextElementSibling); // Next sibling element
console.log(element.previousSibling); // Previous sibling node
console.log(element.previousElementSibling); // Previous sibling element

// Practical traversal example
function findNestedValue(container, selector) {
  const target = container.querySelector(selector);
  if (!target) return null;

  // Traverse up to find data container
  const dataContainer = target.closest("[data-value]");
  return dataContainer ? dataContainer.dataset.value : null;
}

// Tree walking
function walkDOM(node, callback) {
  callback(node);
  node = node.firstChild;
  while (node) {
    walkDOM(node, callback);
    node = node.nextSibling;
  }
}

Asynchronous JavaScript & APIs

Fetch API

Modern way to make HTTP requests, replacing XMLHttpRequest.

// Basic GET request
fetch("https://api.example.com/users")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log("Users:", data);
  })
  .catch((error) => {
    console.error("Fetch error:", error);
  });

// POST request with data
const userData = {
  name: "John Doe",
  email: "john@example.com",
};

fetch("https://api.example.com/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer token123",
  },
  body: JSON.stringify(userData),
})
  .then((response) => response.json())
  .then((data) => console.log("Created user:", data))
  .catch((error) => console.error("Error:", error));

// Modern async/await syntax
async function fetchUsers() {
  try {
    const response = await fetch("https://api.example.com/users");

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const users = await response.json();
    return users;
  } catch (error) {
    console.error("Failed to fetch users:", error);
    throw error;
  }
}

// Error handling and response types
async function handleDifferentResponses(url) {
  const response = await fetch(url);

  // Check content type
  const contentType = response.headers.get("Content-Type");

  if (contentType.includes("application/json")) {
    return await response.json();
  } else if (contentType.includes("text/")) {
    return await response.text();
  } else {
    return await response.blob(); // For files/images
  }
}

Important Notes:

  • fetch() only rejects on network errors, not HTTP error status codes
  • Always check response.ok for successful responses
  • Cross-origin requests require proper CORS configuration
  • Returns a Promise that resolves to the Response object

Event Loop

Understanding JavaScript’s asynchronous execution model.

// Call Stack demonstration
console.log("1"); // Synchronous

setTimeout(() => {
  console.log("2"); // Asynchronous (Web API � Callback Queue)
}, 0);

Promise.resolve().then(() => {
  console.log("3"); // Asynchronous (Microtask Queue)
});

console.log("4"); // Synchronous

// Output: 1, 4, 3, 2

// Event Loop components explained:
// 1. Call Stack: Where function calls are executed
// 2. Web APIs: Browser-provided APIs (setTimeout, DOM events, fetch)
// 3. Callback Queue: Where callbacks wait to be executed
// 4. Microtask Queue: Higher priority queue for Promises
// 5. Event Loop: Monitors Call Stack and queues

// Practical example: Understanding execution order
function demonstrateEventLoop() {
  console.log("Start");

  // Macro task (goes to callback queue)
  setTimeout(() => console.log("Timeout 1"), 0);
  setTimeout(() => console.log("Timeout 2"), 0);

  // Micro task (goes to microtask queue)
  Promise.resolve().then(() => console.log("Promise 1"));
  Promise.resolve().then(() => console.log("Promise 2"));

  console.log("End");
}

// Output: Start, End, Promise 1, Promise 2, Timeout 1, Timeout 2

Event Delegation

Efficient event handling technique using event bubbling.

// Instead of adding listeners to each item
const items = document.querySelectorAll(".list-item");
items.forEach((item) => {
  item.addEventListener("click", handleItemClick); // Multiple listeners
});

// Use event delegation on parent container
const list = document.querySelector(".item-list");
list.addEventListener("click", function (event) {
  // Check if clicked element matches target
  if (event.target.matches(".list-item")) {
    handleItemClick(event);
  }

  // Handle different element types
  if (event.target.matches(".delete-btn")) {
    handleDelete(event);
  }

  if (event.target.matches(".edit-btn")) {
    handleEdit(event);
  }
});

// Advanced delegation with closest()
document.addEventListener("click", function (event) {
  // Handle any button click in the document
  const button = event.target.closest("button");
  if (!button) return;

  // Get button type from data attribute
  const action = button.dataset.action;

  switch (action) {
    case "save":
      handleSave(button);
      break;
    case "cancel":
      handleCancel(button);
      break;
    case "delete":
      handleDelete(button);
      break;
  }
});

// Benefits of event delegation:
// 1. Better performance (fewer event listeners)
// 2. Handles dynamically added elements automatically
// 3. Cleaner code organization
// 4. Reduced memory usage

Browser Storage

LocalStorage vs SessionStorage vs Cookies

// LocalStorage - persistent across browser sessions
localStorage.setItem("username", "johndoe");
localStorage.setItem("preferences", JSON.stringify({ theme: "dark", lang: "en" }));

// Retrieve data
const username = localStorage.getItem("username");
const preferences = JSON.parse(localStorage.getItem("preferences") || "{}");

// SessionStorage - persists only for current tab
sessionStorage.setItem("tempData", "session-specific");
const tempData = sessionStorage.getItem("tempData");

// Storage methods
localStorage.removeItem("username"); // Remove specific item
localStorage.clear(); // Remove all items
console.log(localStorage.length); // Number of items
console.log(localStorage.key(0)); // Get key by index

// Storage utility functions
const storage = {
  set(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error("Storage failed:", e);
    }
  },

  get(key, defaultValue = null) {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch (e) {
      console.error("Storage retrieval failed:", e);
      return defaultValue;
    }
  },

  remove(key) {
    localStorage.removeItem(key);
  },

  clear() {
    localStorage.clear();
  },
};

// Listen for storage changes (across tabs)
window.addEventListener("storage", function (e) {
  console.log("Storage changed:", {
    key: e.key,
    oldValue: e.oldValue,
    newValue: e.newValue,
    url: e.url,
  });
});

Storage Comparison

FeatureCookiesLocalStorageSessionStorage
Capacity4KB5-10MB5-10MB
PersistenceConfigurable expiryUntil clearedUntil tab closes
Server AccessSent with HTTP requestsJavaScript onlyJavaScript only
ScopeDomain/pathDomainDomain + tab
Use CasesAuthenticationUser preferencesTemporary data

Interview Question: When would you use localStorage vs sessionStorage vs cookies?

Answer:

  • Cookies: Authentication tokens, server-side data (sent with requests)
  • LocalStorage: User preferences, cached data that persists across sessions
  • SessionStorage: Form data, temporary state that shouldn’t persist

Performance & Best Practices

DOM Manipulation Performance

// Bad: Multiple DOM queries and modifications
function inefficientDOMUpdate(items) {
  const container = document.querySelector("#container");

  // Multiple reflows and repaints
  items.forEach((item) => {
    const div = document.createElement("div");
    div.textContent = item.name;
    div.className = "item";
    container.appendChild(div); // DOM modification in loop
  });
}

// Good: Batch DOM operations
function efficientDOMUpdate(items) {
  const container = document.querySelector("#container");
  const fragment = document.createDocumentFragment();

  // Build structure in memory
  items.forEach((item) => {
    const div = document.createElement("div");
    div.textContent = item.name;
    div.className = "item";
    fragment.appendChild(div);
  });

  // Single DOM modification
  container.appendChild(fragment);
}

// Even better: Template-based approach
function templateBasedUpdate(items) {
  const container = document.querySelector("#container");

  const html = items.map((item) => `<div class="item">${item.name}</div>`).join("");

  container.innerHTML = html;
}

Memory Management

// Prevent memory leaks
class ComponentManager {
  constructor() {
    this.listeners = [];
  }

  addListener(element, event, handler) {
    element.addEventListener(event, handler);
    this.listeners.push({ element, event, handler });
  }

  // Clean up when component is destroyed
  destroy() {
    this.listeners.forEach(({ element, event, handler }) => {
      element.removeEventListener(event, handler);
    });
    this.listeners = [];
  }
}

// Avoid circular references
function createCircularReference() {
  const parent = document.createElement("div");
  const child = document.createElement("div");

  // This creates a potential memory leak
  parent.child = child;
  child.parent = parent; // Circular reference

  // Modern browsers handle this, but be aware
}