Skip to content

Location and History Objects: URL Operations and History Management

The Navigation Twins

In the browser's navigation system, location and history are a pair of closely cooperating objects. location is responsible for the current page's URL information and navigation operations—telling you "where you are now, where you want to go"; history is responsible for managing browsing history—telling you "where you came from, whether you can go back."

The cooperation of these two objects is like a car's navigation system and driving recorder:

  • Location: Real-time navigation, knows current location and destination, can immediately go to new places
  • History: Driving record, records routes taken, can go back the way came

Mastering these two objects is key to implementing modern web application navigation, especially for single-page application (SPA) routing.

Location Object Detailed

The location object contains the current page's URL information and provides methods to navigate to new pages.

URL Components

A complete URL contains multiple parts, and the location object categorizes them:

https://user:[email protected]:8080/path/page.html?id=123&name=test#section
└─┬─┘   └───┬───┘ └────┬──────┘└┬─┘ └──────┬───────┘ └─────┬──────┘ └──┬───┘
protocol  auth     hostname   port     pathname        search        hash
          └────────────┬────────────┘
                      host

Let's see how to access these parts:

javascript
// Assume current URL is:
// https://example.com:8080/products/laptop.html?id=123&color=blue#specs

// Complete URL
console.log(location.href);
// 'https://example.com:8080/products/laptop.html?id=123&color=blue#specs'

// Protocol (including colon)
console.log(location.protocol);
// 'https:'

// Hostname + port
console.log(location.host);
// 'example.com:8080'

// Hostname (excluding port)
console.log(location.hostname);
// 'example.com'

// Port
console.log(location.port);
// '8080'

// Path (from root directory)
console.log(location.pathname);
// '/products/laptop.html'

// Query string (including question mark)
console.log(location.search);
// '?id=123&color=blue'

// Anchor/hash (including hash mark)
console.log(location.hash);
// '#specs'

// Origin (protocol + hostname + port)
console.log(location.origin);
// 'https://example.com:8080'

Practical Application: URL Parser

javascript
class URLParser {
  constructor(url = window.location.href) {
    this.url = new URL(url);
  }

  // Get all URL information
  getAll() {
    return {
      href: this.url.href,
      protocol: this.url.protocol,
      host: this.url.host,
      hostname: this.url.hostname,
      port: this.url.port,
      pathname: this.url.pathname,
      search: this.url.search,
      hash: this.url.hash,
      origin: this.url.origin,
    };
  }

  // Parse query parameters
  getParams() {
    const params = {};

    for (const [key, value] of this.url.searchParams.entries()) {
      // Handle duplicate parameter names
      if (params[key]) {
        if (Array.isArray(params[key])) {
          params[key].push(value);
        } else {
          params[key] = [params[key], value];
        }
      } else {
        params[key] = value;
      }
    }

    return params;
  }

  // Get single parameter
  getParam(name) {
    return this.url.searchParams.get(name);
  }

  // Get all parameters with same name
  getAllParams(name) {
    return this.url.searchParams.getAll(name);
  }

  // Check if parameter exists
  hasParam(name) {
    return this.url.searchParams.has(name);
  }

  // Format display
  display() {
    console.log("=== URL Information ===");
    const info = this.getAll();

    Object.entries(info).forEach(([key, value]) => {
      if (value) {
        console.log(`${key}: ${value}`);
      }
    });

    const params = this.getParams();
    if (Object.keys(params).length > 0) {
      console.log("\n=== Query Parameters ===");
      Object.entries(params).forEach(([key, value]) => {
        console.log(`${key}: ${value}`);
      });
    }
  }
}

// Usage
const parser = new URLParser(
  "https://example.com/search?q=javascript&category=tutorials&sort=date"
);
parser.display();

console.log("Query word:", parser.getParam("q")); // 'javascript'
console.log("Category:", parser.getParam("category")); // 'tutorials'
console.log("All parameters:", parser.getParams());
// { q: 'javascript', category: 'tutorials', sort: 'date' }

Query Parameter Operations

javascript
class QueryParams {
  // Parse query string
  static parse(search = location.search) {
    const params = new URLSearchParams(search);
    const result = {};

    for (const [key, value] of params.entries()) {
      result[key] = value;
    }

    return result;
  }

  // Build query string
  static stringify(params) {
    const searchParams = new URLSearchParams();

    Object.entries(params).forEach(([key, value]) => {
      if (Array.isArray(value)) {
        value.forEach((v) => searchParams.append(key, v));
      } else if (value !== null && value !== undefined) {
        searchParams.append(key, value);
      }
    });

    return searchParams.toString();
  }

  // Add or update parameter
  static add(name, value) {
    const params = this.parse();
    params[name] = value;
    return this.stringify(params);
  }

  // Delete parameter
  static remove(name) {
    const params = this.parse();
    delete params[name];
    return this.stringify(params);
  }

  // Batch update parameters
  static update(updates) {
    const params = { ...this.parse(), ...updates };
    return this.stringify(params);
  }

  // Clear all parameters
  static clear() {
    return "";
  }
}

// Usage examples
// Current URL: https://example.com/products?category=laptop&price=1000

// Parse current parameters
const params = QueryParams.parse();
console.log("Current parameters:", params);
// { category: 'laptop', price: '1000' }

// Add new parameter
const newQuery = QueryParams.add("color", "blue");
console.log("Add color:", newQuery);
// 'category=laptop&price=1000&color=blue'

// Batch update parameters
const updatedQuery = QueryParams.update({
  price: "1500",
  brand: "TechCorp",
});
console.log("After update:", updatedQuery);
// 'category=laptop&price=1500&brand=TechCorp'

// Delete parameter
const queryWithoutPrice = QueryParams.remove("price");
console.log("Remove price:", queryWithoutPrice);
// 'category=laptop&brand=TechCorp'

The location object provides several ways to navigate to new pages:

javascript
// 1. Directly modify href (most common)
location.href = "https://example.com/newpage";

// 2. assign() method (equivalent to modifying href)
location.assign("https://example.com/newpage");

// 3. replace() method (doesn't leave record in history)
location.replace("https://example.com/newpage");

// 4. reload() method (reload current page)
location.reload(); // May load from cache
location.reload(true); // Force load from server (deprecated, effect not guaranteed)

// Compare assign and replace
function navigateWithAssign() {
  location.assign("/page2");
  // User can click back button to return
}

function navigateWithReplace() {
  location.replace("/page2");
  // User clicking back will skip current page
}

Practical Application: Navigation Manager

javascript
class NavigationManager {
  // Safe navigation (check URL)
  static navigateTo(url, options = {}) {
    const {
      newTab = false,
      replace = false,
      confirm = false,
      confirmMessage = "Are you sure you want to leave this page?",
    } = options;

    // Optional confirmation dialog
    if (confirm && !window.confirm(confirmMessage)) {
      return false;
    }

    // Open in new tab
    if (newTab) {
      window.open(url, "_blank", "noopener,noreferrer");
      return true;
    }

    // Replace current history
    if (replace) {
      location.replace(url);
    } else {
      location.assign(url);
    }

    return true;
  }

  // Navigate to relative path
  static navigateToPath(path, options = {}) {
    const url = `${location.origin}${path}`;
    return this.navigateTo(url, options);
  }

  // Navigate with parameters
  static navigateWithParams(path, params, options = {}) {
    const queryString = QueryParams.stringify(params);
    const url = queryString ? `${path}?${queryString}` : path;
    return this.navigateToPath(url, options);
  }

  // Update URL parameters (without page refresh)
  static updateParams(params, options = {}) {
    const { replace = false } = options;
    const newQuery = QueryParams.update(params);
    const newUrl = `${location.pathname}${newQuery ? "?" + newQuery : ""}${
      location.hash
    }`;

    if (replace) {
      history.replaceState(null, "", newUrl);
    } else {
      history.pushState(null, "", newUrl);
    }
  }

  // Reload page (with options)
  static reloadPage(options = {}) {
    const {
      clearCache = false,
      confirm = false,
      confirmMessage = "Are you sure you want to reload this page?",
    } = options;

    if (confirm && !window.confirm(confirmMessage)) {
      return false;
    }

    if (clearCache) {
      // Add timestamp to bypass cache
      const timestamp = Date.now();
      const separator = location.search ? "&" : "?";
      location.href = `${location.pathname}${location.search}${separator}_t=${timestamp}${location.hash}`;
    } else {
      location.reload();
    }

    return true;
  }

  // Navigate to anchor
  static scrollToHash(hash, smooth = true) {
    const element = document.getElementById(hash.replace("#", ""));

    if (element) {
      element.scrollIntoView({
        behavior: smooth ? "smooth" : "auto",
        block: "start",
      });

      // Update URL hash without triggering page jump
      if (history.pushState) {
        history.pushState(null, "", `#${hash.replace("#", "")}`);
      } else {
        location.hash = hash;
      }
    }
  }
}

// Usage examples

// Navigate to new page
NavigationManager.navigateTo("/products");

// Open in new tab
NavigationManager.navigateTo("https://example.com", { newTab: true });

// Navigate with confirmation
NavigationManager.navigateTo("/logout", {
  confirm: true,
  confirmMessage: "Are you sure you want to logout?",
  replace: true,
});

// Navigate with parameters
NavigationManager.navigateWithParams("/search", {
  q: "javascript",
  category: "tutorials",
  page: 1,
});

// Update URL parameters (without page refresh)
NavigationManager.updateParams({ page: 2, sort: "date" });

// Reload and clear cache
NavigationManager.reloadPage({ clearCache: true });

// Smooth scroll to anchor
NavigationManager.scrollToHash("#section-2");

History Object Detailed

The history object manages the browser's session history, providing methods to move forward and backward in history, as well as HTML5's new state management API.

Basic Navigation Methods

javascript
// History length
console.log(history.length); // Number of history entries in current session

// Go back one page (equivalent to clicking browser back button)
history.back();

// Go forward one page (equivalent to clicking browser forward button)
history.forward();

// Jump to specified position
history.go(-1); // Go back one page (same as back())
history.go(1); // Go forward one page (same as forward())
history.go(-2); // Go back two pages
history.go(0); // Refresh current page

// Practical application: Navigation buttons
function createNavigationButtons() {
  const backBtn = document.getElementById("backBtn");
  const forwardBtn = document.getElementById("forwardBtn");

  backBtn.addEventListener("click", () => {
    if (history.length > 1) {
      history.back();
    } else {
      console.log("No page to go back to");
    }
  });

  forwardBtn.addEventListener("click", () => {
    history.forward();
  });
}

HTML5 History API

HTML5 introduced a powerful History API that allows managing history and URLs without refreshing the page, which is the foundation for implementing single-page application (SPA) routing.

pushState() - Add History Entry

javascript
// pushState() adds a new history entry
// Syntax: history.pushState(state, title, url)

// Add simple history entry
history.pushState(
  { page: 1 }, // state: State object (can store any data)
  "", // title: Title (most browsers currently ignore this parameter)
  "/page1" // url: New URL (must be same-origin)
);

// Add history entry with complex state
history.pushState(
  {
    page: "products",
    filter: { category: "laptop", price: 1000 },
    timestamp: Date.now(),
  },
  "",
  "/products?category=laptop&price=1000"
);

// Note: pushState doesn't trigger page refresh
console.log("URL updated but page didn't refresh");

replaceState() - Replace History Entry

javascript
// replaceState() replaces current history entry
// Syntax: history.replaceState(state, title, url)

history.replaceState({ page: "home" }, "", "/home");

// Difference from pushState:
// - pushState: Adds new entry, can go back to previous page
// - replaceState: Replaces current entry, cannot go back to previous page

state Property

javascript
// Get current history's state object
console.log(history.state); // State set by most recent pushState/replaceState

// Example
history.pushState({ user: "John", id: 123 }, "", "/user/123");
console.log(history.state); // { user: 'John', id: 123 }

popstate Event

javascript
// Listen for history changes (triggered when user clicks back/forward)
window.addEventListener("popstate", (event) => {
  console.log("History changed");
  console.log("New state:", event.state);
  console.log("New URL:", location.href);

  // Update page content based on state
  if (event.state) {
    updatePageContent(event.state);
  }
});

function updatePageContent(state) {
  console.log("Update page based on state:", state);
  // In real application, render corresponding page content here
}

Practical Application: Simple SPA Router

javascript
class SimpleRouter {
  constructor(routes) {
    this.routes = routes; // { '/': handler, '/about': handler }
    this.currentPath = null;
    this.init();
  }

  init() {
    // Listen for browser back/forward
    window.addEventListener("popstate", (event) => {
      this.handleRoute(location.pathname, event.state);
    });

    // Intercept link clicks
    document.addEventListener("click", (event) => {
      // Check if clicked element has data-link attribute
      const link = event.target.closest("[data-link]");

      if (link) {
        event.preventDefault();
        const path = link.getAttribute("href");
        this.navigate(path);
      }
    });

    // Handle initial route
    this.handleRoute(location.pathname);
  }

  navigate(path, state = {}) {
    // Avoid duplicate navigation
    if (path === this.currentPath) {
      return;
    }

    // Update history
    history.pushState(state, "", path);

    // Handle route
    this.handleRoute(path, state);
  }

  replace(path, state = {}) {
    // Replace current history
    history.replaceState(state, "", path);
    this.handleRoute(path, state);
  }

  handleRoute(path, state = null) {
    console.log(`Route changed: ${path}`);

    this.currentPath = path;

    // Find matching route
    const handler = this.routes[path] || this.routes["/404"];

    if (handler) {
      // Call route handler
      handler(state);
    } else {
      console.error(`Route not found: ${path}`);
    }
  }

  back() {
    history.back();
  }

  forward() {
    history.forward();
  }
}

// Usage example
const router = new SimpleRouter({
  "/": (state) => {
    console.log("Home page");
    document.getElementById("app").innerHTML = `
      <h1>Home</h1>
      <nav>
        <a href="/about" data-link>About</a>
        <a href="/products" data-link>Products</a>
        <a href="/contact" data-link>Contact</a>
      </nav>
    `;
  },

  "/about": (state) => {
    console.log("About page");
    document.getElementById("app").innerHTML = `
      <h1>About Us</h1>
      <p>Welcome to TechCorp</p>
      <a href="/" data-link>Back to Home</a>
    `;
  },

  "/products": (state) => {
    console.log("Products page", state);
    const filter = state?.filter || {};

    document.getElementById("app").innerHTML = `
      <h1>Product List</h1>
      <p>Current filter: ${JSON.stringify(filter)}</p>
      <button id="laptopBtn">Laptop</button>
      <button id="phoneBtn">Phone</button>
      <a href="/" data-link>Back to Home</a>
    `;

    document.getElementById("laptopBtn")?.addEventListener("click", () => {
      router.navigate("/products", {
        filter: { category: "laptop" },
      });
    });

    document.getElementById("phoneBtn")?.addEventListener("click", () => {
      router.navigate("/products", {
        filter: { category: "phone" },
      });
    });
  },

  "/contact": (state) => {
    console.log("Contact page");
    document.getElementById("app").innerHTML = `
      <h1>Contact Us</h1>
      <p>Email: [email protected]</p>
      <a href="/" data-link>Back to Home</a>
    `;
  },

  "/404": (state) => {
    console.log("404 page");
    document.getElementById("app").innerHTML = `
      <h1>404 - Page Not Found</h1>
      <a href="/" data-link>Back to Home</a>
    `;
  },
});

// Programmatic navigation
// router.navigate('/about');

Practical Application: Advanced Router Manager

javascript
class Router {
  constructor() {
    this.routes = [];
    this.beforeHooks = [];
    this.afterHooks = [];
    this.currentRoute = null;
    this.init();
  }

  init() {
    window.addEventListener("popstate", (event) => {
      this.handleRoute(location.pathname, event.state);
    });

    document.addEventListener("click", (event) => {
      const link = event.target.closest("[data-link]");

      if (link) {
        event.preventDefault();
        this.push(link.getAttribute("href"));
      }
    });

    this.handleRoute(location.pathname);
  }

  // Register route
  register(path, handler, options = {}) {
    this.routes.push({
      path,
      handler,
      pattern: this.pathToRegex(path),
      ...options,
    });

    return this;
  }

  // Convert path pattern to regex
  pathToRegex(path) {
    // Support dynamic routes, like /user/:id
    const pattern = path
      .replace(/\//g, "\\/")
      .replace(/:(\w+)/g, "(?<$1>[^/]+)");

    return new RegExp(`^${pattern}$`);
  }

  // Match route
  matchRoute(path) {
    for (const route of this.routes) {
      const match = path.match(route.pattern);

      if (match) {
        return {
          route,
          params: match.groups || {},
        };
      }
    }

    return null;
  }

  // Before hooks
  beforeEach(hook) {
    this.beforeHooks.push(hook);
    return this;
  }

  // After hooks
  afterEach(hook) {
    this.afterHooks.push(hook);
    return this;
  }

  // Execute before hooks
  async runBeforeHooks(to, from) {
    for (const hook of this.beforeHooks) {
      const result = await hook(to, from);

      // If hook returns false, interrupt navigation
      if (result === false) {
        return false;
      }

      // If hook returns string, redirect to that path
      if (typeof result === "string") {
        this.push(result);
        return false;
      }
    }

    return true;
  }

  // Execute after hooks
  async runAfterHooks(to, from) {
    for (const hook of this.afterHooks) {
      await hook(to, from);
    }
  }

  // Handle route
  async handleRoute(path, state = null) {
    const matched = this.matchRoute(path);

    if (!matched) {
      console.error(`Route not found: ${path}`);
      return;
    }

    const to = {
      path,
      params: matched.params,
      query: QueryParams.parse(),
      state,
    };

    const from = this.currentRoute;

    // Execute before hooks
    const shouldContinue = await this.runBeforeHooks(to, from);

    if (!shouldContinue) {
      return;
    }

    // Execute route handler
    await matched.route.handler(to);

    // Update current route
    this.currentRoute = to;

    // Execute after hooks
    await this.runAfterHooks(to, from);
  }

  // Add history entry
  push(path, state = {}) {
    history.pushState(state, "", path);
    this.handleRoute(path, state);
  }

  // Replace history entry
  replace(path, state = {}) {
    history.replaceState(state, "", path);
    this.handleRoute(path, state);
  }

  // Back
  back() {
    history.back();
  }

  // Forward
  forward() {
    history.forward();
  }
}

// Usage example
const router = new Router();

// Register routes (support dynamic parameters)
router
  .register("/", async (route) => {
    console.log("Home page");
    document.getElementById("app").innerHTML = "<h1>Home</h1>";
  })
  .register("/user/:id", async (route) => {
    console.log("User page", route.params);
    const userId = route.params.id;

    document.getElementById("app").innerHTML = `
      <h1>User: ${userId}</h1>
      <p>Query parameters: ${JSON.stringify(route.query)}</p>
    `;
  })
  .register("/product/:category/:id", async (route) => {
    console.log("Product page", route.params);
    const { category, id } = route.params;

    document.getElementById("app").innerHTML = `
      <h1>Product</h1>
      <p>Category: ${category}</p>
      <p>ID: ${id}</p>
    `;
  });

// Before hooks (for permission checks, etc.)
router.beforeEach((to, from) => {
  console.log(`Navigation: ${from?.path || "(initial)"} -> ${to.path}`);

  // Example: Check authentication
  if (to.path.startsWith("/admin")) {
    const isAuthenticated = false; // In real application, check user state

    if (!isAuthenticated) {
      console.log("Not authenticated, redirect to login page");
      return "/login"; // Redirect
    }
  }

  // Return undefined or true to continue navigation
});

// After hooks (for analytics, etc.)
router.afterEach((to, from) => {
  console.log("Navigation complete:", to.path);

  // Send page view statistics
  // analytics.pageview(to.path);
});

// Programmatic navigation
// router.push('/user/123');
// router.push('/product/laptop/456');
// router.replace('/about');

Best Practices

1. Safe URL Operations

javascript
// ❌ Unsafe: Direct URL concatenation
const url = "https://example.com/search?q=" + userInput;

// ✅ Safe: Use URLSearchParams
const params = new URLSearchParams();
params.set("q", userInput); // Automatically encodes
const url = `https://example.com/search?${params}`;

// ✅ Better: Use URL object
const url = new URL("https://example.com/search");
url.searchParams.set("q", userInput);
console.log(url.href);

2. Same-Origin Policy Compliance

javascript
// pushState/replaceState can only be used for same-origin URLs
try {
  history.pushState(null, "", "https://another-domain.com"); // ❌ Cross-origin error
} catch (e) {
  console.error("Cross-origin error:", e);
}

// ✅ Correct: Same-origin URL
history.pushState(null, "", "/new-path"); // Path under same domain

3. State Object Size Limitations

javascript
// ⚠️ Note: Don't store large amounts of data in state object
// Different browsers have limits on state object size (usually 640KB - 10MB)

// ❌ Not recommended: Store large amounts of data
history.pushState(
  {
    user: {
      /* lots of user data */
    },
    products: [
      /* thousands of products */
    ],
  },
  "",
  "/page"
);

// ✅ Recommended: Only store necessary identifiers
history.pushState(
  {
    userId: 123,
    page: "products",
    filters: { category: "laptop" },
  },
  "",
  "/products"
);

// Large amounts of data should be stored elsewhere (localStorage, IndexedDB, etc.)

4. Gracefully Handle Back Button

javascript
// Prevent accidental user exit (if there are unsaved changes)
window.addEventListener("beforeunload", (event) => {
  const hasUnsavedChanges = true; // In real application, check state

  if (hasUnsavedChanges) {
    const message = "You have unsaved changes. Are you sure you want to leave?";
    event.returnValue = message;
    return message;
  }
});

// Custom back behavior
let canGoBack = true;

window.addEventListener("popstate", (event) => {
  if (!canGoBack) {
    // If not allowed to go back, immediately forward again
    history.forward();

    // Prompt user
    alert("Please complete current operation first");
  }
});

Summary

location and history objects are the core of the browser navigation system:

Location Object:

  • URL information (href, protocol, host, pathname, search, hash)
  • Query parameter parsing (URLSearchParams)
  • Page navigation (assign(), replace(), reload())

History Object:

  • History navigation (back(), forward(), go())
  • HTML5 History API (pushState(), replaceState(), state)
  • popstate event listening

Best Practices:

  1. Use URLSearchParams to safely handle query parameters
  2. Comply with same-origin policy
  3. Limit state object size
  4. Use pushState and replaceState appropriately
  5. Gracefully handle browser back operations

Mastering these two objects, especially the HTML5 History API, is the foundation for building modern single-page applications (SPA). They enable you to achieve smooth navigation experiences without page refreshes.