Location, Navigation, History & Geolocation
|
This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every browser window exposes two objects for working with "where am I and how did I get here":
window.location (aliased as document.location), which represents the current URL and can load a new one,
and window.history, which models the list of documents and states the user has navigated through. Together
they are what single-page applications lean on to change what’s on screen while keeping the URL bar, and the
Back/Forward buttons, in sync. This page closes with the Geolocation API, a physically-unrelated but
commonly-grouped "where is the user" API that the book does not cover.
window.location
The location property of both the Window and Document objects refers to a Location object representing
the current URL of the document displayed in the window. It behaves much like a URL object (see
Networking): its properties expose the individual parts of the
current URL, and assigning to it (or to some of its properties) navigates the browser.
Parsing the Current URL
// Given the current URL http://example.com:8080/products/42?sort=price#reviews
location.protocol; // "http:"
location.host; // "example.com:8080"
location.hostname; // "example.com"
location.port; // "8080"
location.pathname; // "/products/42"
location.search; // "?sort=price"
location.hash; // "#reviews"
location.href; // the entire URL as a string (same as location.toString())
The hash property returns the URL’s fragment identifier — a # followed by an element ID, traditionally used
to scroll the page to that element — and search returns the query-string portion, starting with ?. Both are
meant for embedding arguments in the URL; while the arguments are usually intended for a server, nothing stops
JavaScript from reading and using them too.
The Location object itself has no searchParams property the way a URL object does, but constructing a
URL from it gets you one:
let url = new URL(window.location);
let query = url.searchParams.get("q");
let numResults = parseInt(url.searchParams.get("n") || "10");
document.URL is a related but easily confused property: despite the name, its value is a plain string
holding the current document’s URL, not a URL object.
Navigating to New Documents
Assigning a string to window.location or document.location is interpreted as a URL and tells the browser to
load it, replacing the current document:
window.location = "http://www.example.com"; // navigate to a new page
document.location = "page2.html"; // relative URLs are resolved against the current URL
A bare fragment identifier is a special case: assigning it doesn’t load a new document, it just scrolls so the
element whose id/name matches the fragment is visible at the top of the window (#top scrolls to the very
start of the document, even without a matching element):
location = "#top"; // jump to the top of the current document
The individual Location properties are themselves writable, and setting them navigates too (or, for hash,
scrolls within the current document without a full navigation):
location.pathname = "pages/3.html"; // load a new page
location.hash = "toc"; // scroll to the element with id="toc"
location.search = "?page=" + (page + 1); // reload with a new query string
location.assign(url) does the same thing as assigning a string to location directly — it’s provided mostly
for symmetry with replace(). location.replace(url) also loads a new page, but with one important difference:
it replaces the current document in the browser’s history instead of adding to it. If script in document A
navigates to document B via location = "B.html" and the user then clicks Back, the browser returns to document
A. If A had used location.replace("B.html") instead, A is erased from history, and Back takes the user to
whatever page preceded A. This matters for any unconditional redirect — e.g. bouncing unsupported browsers to
a static fallback page — where you don’t want the Back button to bounce the user right back into the redirect:
// An unconditional redirect should use replace(), not assign a new location() /
// location.href directly -- otherwise Back re-triggers the same redirect.
if (!isBrowserSupported()) {
location.replace("static-fallback.html");
}
The History API
The history property of Window refers to the History object, which models the browsing history of the
window as a list of documents and document states. history.length reports how many entries that list has, but
for security reasons scripts cannot read the stored URLs themselves — otherwise any script could snoop through
a user’s browsing history.
Browsing History: back(), forward(), and go()
History has back() and forward() methods that behave exactly like the browser’s own Back and Forward
buttons, plus a more general go(delta) that jumps any number of steps in either direction:
history.back(); // go back one entry, like clicking Back
history.forward(); // go forward one entry
history.go(-2); // go back two entries at once
history.go(0); // an alternate way to reload the current page
If a window contains child windows (such as <iframe> elements), their histories are chronologically
interleaved with the main window’s, so history.back() on the main window can end up navigating a child frame
back instead of the top-level page, leaving the main page’s own state unchanged.
Managing History Without Reloading: pushState() and replaceState()
The History object described above dates back to when documents were passive and all computation happened on
the server. Modern web apps generate and swap content dynamically without loading new documents at all, and if
they still want the Back/Forward buttons to work intuitively across those in-app "pages," they have to manage
history themselves. The book covers two techniques for this; the modern, non-hacky one is built on
history.pushState() and the popstate event. (An older technique based on location.hash and the
hashchange event still works and is simpler, but is a repurposing of the fragment identifier rather than a
purpose-built API, and is largely superseded by pushState() today.)
When an app enters a new logical state, it calls pushState() to add an object representing that state to the
browser’s history:
history.pushState(stateObject, "", url);
-
stateObject— any data needed to restore this state later. It is serialized with the HTML structured clone algorithm, which is more capable thanJSON.stringify(): it also handlesMap,Set,Date,RegExp, and typed arrays, and can cope with circular references. It cannot serialize functions or classes, and cloning an instance of a user-defined class loses its prototype — it comes back as a plain object. -
title— intended as a title string for the state, but unsupported by most browsers in practice; pass an empty string. -
url— an optional URL to display in the address bar immediately, and again if the user returns to this state via Back/Forward. Relative URLs resolve against the document’s current location. Giving each state its own URL is what lets users bookmark or share a link to a specific in-app state.
history.replaceState() takes the same three arguments but replaces the current history entry instead of
adding a new one — it’s typically called once, right after the app first loads, to attach a state object to the
initial page instead of leaving it without one.
The popstate Event and a Minimal SPA Router
When the user navigates to a saved history state via Back/Forward, the browser fires a popstate event on
window. The event’s state property holds a (structured-clone) copy of whatever object was passed to
pushState() for that entry — note that visiting a bookmarked/shared URL directly does not fire popstate;
in that case the app has to reconstruct its state by parsing the URL instead.
Together, pushState() + popstate are exactly the primitive a single-page-app router needs: intercept link
clicks, update the URL and push a state entry instead of letting the browser navigate, and render whatever the
new URL maps to — then do the same rendering in the popstate handler so Back/Forward reproduce it:
const routes = {
"/": () => renderHome(),
"/products": () => renderProductList(),
"/about": () => renderAbout(),
};
function renderRoute(path) {
(routes[path] || (() => render404()))();
}
// Programmatic navigation: push a new entry and render it immediately.
function navigate(path) {
history.pushState({ path }, "", path);
renderRoute(path);
}
// Intercept clicks on same-origin links so they go through the router
// instead of triggering a full page load.
document.addEventListener("click", (event) => {
const link = event.target.closest("a[href]");
if (link && link.origin === location.origin) {
event.preventDefault();
navigate(link.pathname);
}
});
// Back/Forward: the browser restores the URL on its own, but rendering
// the corresponding view is still the app's responsibility.
window.addEventListener("popstate", (event) => {
renderRoute(event.state ? event.state.path : location.pathname);
});
// Initial load: attach a state object to the page we started on so the
// very first popstate has something to work with.
history.replaceState({ path: location.pathname }, "", location.pathname);
renderRoute(location.pathname);
Every real SPA router (React Router, Vue Router, and so on) is a more feature-complete version of this same
pattern: intercept navigation, call pushState(), re-render from the new URL, and mirror that rendering in a
popstate listener for the Back/Forward case — all without a single full-page reload.
Geolocation API
|
The Geolocation API has no dedicated coverage in this section’s reference material — it’s mentioned only as a further-reading pointer. Everything in this section is drawn from general/official knowledge (MDN). |
navigator.geolocation exposes the browser’s location-services API, letting a page ask for the user’s physical
position (derived from GPS, Wi-Fi/cell-tower positioning, or IP address, depending on the device and what’s
available).
getCurrentPosition(), watchPosition(), and clearWatch()
getCurrentPosition() asks for the position once:
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
console.log(`You are near (${latitude}, ${longitude}), accurate to ${accuracy}m`);
},
(error) => {
console.error(`Geolocation failed: ${error.message}`); // e.g. permission denied, timeout
},
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 },
);
watchPosition() has the same signature but keeps calling the success callback whenever the device’s position
changes meaningfully, until you stop it. It returns a numeric watch ID that clearWatch() uses to cancel the
subscription — the same relationship setInterval()/clearInterval() has:
const watchId = navigator.geolocation.watchPosition(
(position) => updateMapMarker(position.coords),
(error) => console.error(error.message),
{ enableHighAccuracy: true },
);
// Later, e.g. when the user navigates away from the map view:
navigator.geolocation.clearWatch(watchId);
The Permission-Prompt Flow
Because location is sensitive data, browsers only expose it behind a permission prompt, and both
getCurrentPosition() and watchPosition() must be called as the direct result of a user gesture (a click or
tap) in most modern browsers — calling them unconditionally on page load, before the user has done anything, is
a common way to get the prompt auto-dismissed or the call silently blocked. The first call for a given origin
triggers the browser’s native "Allow this site to know your location?" prompt; the user’s choice (allow, block,
or "allow once") is then remembered by the browser for that origin, and can be inspected in advance (without
triggering the prompt) via the Permissions API:
navigator.permissions.query({ name: "geolocation" }).then((status) => {
console.log(status.state); // "granted", "denied", or "prompt"
});
Modern browsers also require a secure context (HTTPS, or localhost during development) to expose
navigator.geolocation at all; calling it from a plain http:// page either does nothing or fails immediately,
regardless of what the user would have chosen.
Accuracy and Privacy Considerations
The options object accepted by getCurrentPosition()/watchPosition() trades accuracy against battery/time
cost:
| Option | Meaning |
|---|---|
|
When |
|
Milliseconds to wait for a position before invoking the error callback with a timeout error. Defaults to
|
|
Milliseconds a cached position is still considered acceptable to return, instead of requesting a fresh one.
|
A device’s reported accuracy (the coords.accuracy field, in meters) varies enormously with the positioning
source in play — from a few meters with GPS outdoors, to tens or hundreds of meters or worse for network-based
positioning indoors — so code should treat the coordinates as an estimate with a radius, not a precise point.
Precise location is one of the most sensitive categories of personal data a page can request: it can reveal
where someone lives, works, or currently is standing, and repeated watchPosition() samples can reconstruct a
movement history. Beyond the mandatory browser permission prompt, applications should ask for it only when a
feature genuinely needs it (not on every page load "just in case"), explain why before triggering the prompt,
avoid retaining raw coordinates longer than the feature requires, and prefer the coarsest enableHighAccuracy/
maximumAge settings that still satisfy the use case.