JavaScript BOM, Modules, and Modern Features
- Description: The Browser Object Model (
window,location,history,navigator, timers, storage, cookies), ES modules, and the modern syntax that defines idiomatic 2026 JavaScript (destructuring, spread, optional chaining, template literals, modules, top-level await). - My Notion Note ID: K2A-F4-8
- Created: 2018-03-23
- Updated: 2026-05-17
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. The BOM in a Sentence
- 2. window
- 3. location
- 4. history
- 5. navigator
- 6. screen and viewport
- 7. Dialogs
- 8. Storage and Cookies
- 9. ES Modules
- 10. Modern Syntax Cheat Sheet
- 11. References
1. The BOM in a Sentence
The BOM (Browser Object Model) is "everything the browser exposes that isn't the DOM" — window, navigator, location, history, screen, timers, dialogs, storage. Not a single spec — assembled from HTML, DOM, Fetch, URL, Storage, Web Storage, and other standards.
window is the global. In a browser, globalThis === window. In Node, globalThis === global. In Workers, globalThis === self.
2. window
Top-level access to browser features. Every global variable becomes a window property:
var pageTitle = 'X';
window.pageTitle; // 'X' — var creates global
// In modules, top-level let/const do NOT attach to window
let x = 1;
window.x; // undefined
Common members:
| Property | Use |
|---|---|
window.innerWidth / innerHeight |
Viewport size (CSS pixels). |
window.scrollX / scrollY |
Scroll offsets. Same as pageXOffset. |
window.devicePixelRatio |
1, 2, 3 — retina detection. |
window.parent, window.top |
Iframe parents. |
window.opener |
Window that opened this one (security: use noopener to prevent). |
window.frames, window.length |
Iframes contained in this window. |
window.crypto |
Web Crypto API (crypto.randomUUID(), subtle for keys/hashes). |
window.performance |
High-resolution timing. |
Methods:
window.scrollTo({ top: 0, behavior: 'smooth' });
window.scrollBy({ top: 100 });
window.print();
window.close();
window.focus();
window.matchMedia('(prefers-color-scheme: dark)').matches;
Events listened to on window:
load,DOMContentLoaded— page lifecycle.beforeunload,unload,pagehide,pageshow— exit lifecycle (BFCache friendly withpagehide).resize,scroll.online,offline.error,unhandledrejection.popstate,hashchange— navigation.storage— fires in other tabs when localStorage changes.message—postMessagefrom iframe / opener / worker.
3. location
Reflects (and mutates) the URL.
location.href; // 'https://example.io/a/b?x=1#frag'
location.protocol; // 'https:'
location.host; // 'example.io' (or 'example.io:8080')
location.hostname; // 'example.io'
location.port; // '' for default ports
location.pathname; // '/a/b'
location.search; // '?x=1'
location.hash; // '#frag'
location.origin; // 'https://example.io'
// Mutate (causes navigation):
location.href = '/new';
location.assign('/new'); // adds history entry
location.replace('/new'); // replaces history entry — no back button
location.reload();
Parse the query string properly with URLSearchParams:
const params = new URLSearchParams(location.search);
params.get('x'); // '1'
params.has('x');
params.set('x', '2');
params.delete('y');
params.toString();
Construct URLs with URL:
const url = new URL('/api/users?page=2', 'https://example.io');
url.searchParams.set('limit', '50');
fetch(url);
4. history
Navigation stack for the current tab.
history.length;
history.back();
history.forward();
history.go(-2); // navigate by offset
// Single-Page App navigation
history.pushState(state, '', '/new-route'); // adds entry, no reload
history.replaceState(state, '', '/route'); // replaces current entry
window.addEventListener('popstate', (e) => {
// Back/forward triggered — read e.state, update UI
});
pushState doesn't fire popstate. Listen and render on push for SPA navigation.
Newer Navigation API (navigation.navigate, navigation.intercept) is gradually replacing pushState. Chromium-only as of 2026; SPA libraries (Next, Remix) abstract it.
5. navigator
Information about the user's browser and device.
navigator.userAgent; // string (mostly useless now; spoofed)
navigator.userAgentData; // structured client hints (Chromium)
navigator.language; // 'en-US'
navigator.languages; // ['en-US', 'en']
navigator.onLine; // boolean (only "no network" hint, not connectivity)
navigator.hardwareConcurrency; // logical CPU count
navigator.deviceMemory; // approximate RAM in GB
navigator.cookieEnabled;
navigator.maxTouchPoints; // 0 = no touch
navigator.clipboard.writeText('hi');
navigator.clipboard.readText();
navigator.share({ title, text, url }); // mobile share sheet
navigator.geolocation.getCurrentPosition(success, error);
navigator.permissions.query({ name: 'clipboard-read' });
navigator.mediaDevices.getUserMedia({ audio: true, video: true });
navigator.serviceWorker.register('/sw.js');
User-Agent sniffing is fragile. Detect features, not browsers:
if ('share' in navigator) navigator.share({ ... });
6. screen and viewport
screen.width; // entire screen width in CSS pixels (NOT the viewport — differs from innerWidth, which measures the browser window)
screen.height;
screen.availWidth; // minus OS UI (taskbar, dock)
screen.availHeight;
screen.colorDepth;
screen.orientation; // { type: 'portrait-primary', angle: 0 }
For element/layout sizing, use getBoundingClientRect (per element) or window.innerWidth (viewport). Don't rely on screen to compute layout — multi-monitor breaks the math.
7. Dialogs
alert('Done'); // OK button — blocking
confirm('Sure?'); // OK / Cancel — returns boolean
prompt('Name?', 'default'); // text input — returns string or null
All three are synchronous and block the page. Annoying for users; many embed contexts (iframes) block them entirely. Replace with a <dialog> element or a framework modal.
<dialog id="modal">
<p>Sure?</p>
<button onclick="modal.close('yes')">Yes</button>
<button onclick="modal.close('no')">No</button>
</dialog>
modal.showModal(); // modal — backdrop, focus trap
const result = await new Promise(resolve => {
modal.addEventListener('close', () => resolve(modal.returnValue), { once: true });
});
8. Storage and Cookies
8.1 Web Storage — localStorage and sessionStorage
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // 'dark'
localStorage.removeItem('theme');
localStorage.clear();
localStorage.length;
localStorage.key(0);
// Strings only — serialise yourself
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Yu' }));
const user = JSON.parse(localStorage.getItem('user') ?? 'null');
| localStorage | sessionStorage | |
|---|---|---|
| Persists across tabs | Yes | No — per tab |
| Persists across sessions | Yes | Until tab closes |
| Capacity | ~5-10 MB | Same |
| Synchronous API | Yes (can block main thread) | Yes |
Cross-tab sync: 'storage' event fires on window in other tabs when localStorage changes.
For larger or async needs: IndexedDB (structured, async, ~50 MB+, indexable). Wrappers like idb make IndexedDB tolerable.
8.2 Cookies
document.cookie; // all cookies as one string — yuck
document.cookie = 'name=Yu; path=/; max-age=86400; secure; samesite=lax';
The API is awful — every set adds one cookie, every get returns everything concatenated. Modern code uses HTTP-only cookies set by the server (auth tokens), not document.cookie. The new Cookie Store API (cookieStore.set / get / delete) is async and sane; Chromium has it, Safari/Firefox progressing.
Cookie attributes:
| Attribute | Purpose |
|---|---|
path=/ |
Cookie scope. |
domain=example.io |
Cross-subdomain. |
max-age=N (seconds) or expires=Date |
Persistence. |
secure |
HTTPS only. |
httponly |
Inaccessible to JS — server only. Auth cookies should set this. |
| `samesite=strict | lax |
9. ES Modules
// math.js
export const PI = 3.14;
export function area(r) { return PI * r * r; }
export default class Circle { /* ... */ }
// main.js
import Circle, { PI, area } from './math.js';
import * as math from './math.js';
import { area as computeArea } from './math.js';
import './side-effects.js'; // run for side effects only
9.1 Static Imports
- Hoisted — all
imports execute before any other code in the module. - File path must be a string literal — no template expressions.
- Each module runs once per realm, regardless of how many places import it.
- The exports are live bindings, not snapshots — re-exports stay in sync.
9.2 Dynamic import()
const module = await import('./chart.js');
const { renderChart } = module;
- Returns a Promise.
- Argument can be an expression — perfect for code splitting and lazy loading.
- Works in both modules and classic scripts.
9.3 Re-exports
export { area } from './math.js';
export * from './math.js';
export { area as compute } from './math.js';
export { default } from './math.js';
9.4 Browser Module Specifiers
import { x } from './math.js'; // relative
import { x } from '/lib/math.js'; // absolute path
import { x } from 'https://cdn/x.js'; // absolute URL
import { x } from 'react'; // bare specifier — needs an import map or bundler
Import maps let plain HTML resolve bare specifiers:
<script type="importmap">
{ "imports": { "react": "https://esm.sh/react@19" } }
</script>
9.5 Node vs Browser
| Node ESM | Browser | |
|---|---|---|
Bare specifier 'react' |
Resolved via node_modules |
Needs import map or bundler |
.json import |
Yes, with attribute (with { type: 'json' }) |
Yes, with attribute |
__dirname / __filename |
Not in ESM. Use import.meta.url. |
Use import.meta.url. |
| Top-level await | Yes | Yes |
| CommonJS interop | require() inside CJS only; ESM uses default export from CJS |
n/a |
// Modern way to get the file URL / dirname in ESM
import.meta.url;
import.meta.dirname; // Node 20.11+ / Deno / Bun (host-specific; not in ECMAScript, not in browsers)
new URL('./data.json', import.meta.url);
10. Modern Syntax Cheat Sheet
A compact reference for the syntactic sugar that defines idiomatic ES2020+. Most appear in other notes — collected here.
10.1 Template Literals
`Hello, ${name}`;
`Total: $${(price * qty).toFixed(2)}`;
// Multi-line
`line 1
line 2`;
// Tagged templates
sql`SELECT * FROM users WHERE id = ${id}`;
10.2 Destructuring
const { a, b, c = 0 } = obj;
const [x, y, ...rest] = arr;
const { a: alias, b: { nested } } = obj;
10.3 Spread and Rest
const merged = { ...a, ...b };
const combined = [...arr1, ...arr2];
function sum(...nums) {}
sum(...[1, 2, 3]);
10.4 Optional Chaining and Nullish Coalescing
user?.address?.city; // undefined if any is nullish
fn?.(arg); // call only if fn exists
arr?.[0]; // index only if arr exists
const port = config.port ?? 3000; // null/undefined → default
config.port ??= 3000; // assign only if nullish
10.5 Logical Assignment
a ||= b; // a = a || b
a &&= b;
a ??= b;
10.6 Numeric Separators
const million = 1_000_000;
const bytes = 0xFF_FF_FF_FF;
10.7 Object Shorthand and Computed Keys
const x = 1, y = 2;
const point = { x, y };
const key = 'dynamic';
const obj = { [key]: 1, [`prefix-${key}`]: 2 };
10.8 Arrow Functions
const square = x => x * x;
const make = () => ({ x: 1 }); // object literal needs parens
arr.map(x => x * 2);
10.9 Top-Level await (ES Modules)
// config.mjs
const data = await fetch('/config.json').then(r => r.json());
export default data;
10.10 Class Fields and Private Members
class Counter {
count = 0;
#secret = 'hush';
static all = new Set();
increment = () => { this.count++; } // auto-bound arrow method
}
10.11 Iterator Helpers (ES2025)
const evens = (function* () { let i = 0; while (true) yield i++; })()
.filter(n => n % 2 === 0)
.take(5)
.toArray();
11. References
- HTML Living Standard — Window — https://html.spec.whatwg.org/multipage/window-object.html
- MDN URL API — https://developer.mozilla.org/en-US/docs/Web/API/URL
- MDN History API — https://developer.mozilla.org/en-US/docs/Web/API/History_API
- MDN Web Storage — https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
- MDN ECMAScript modules — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- Node ESM docs — https://nodejs.org/api/esm.html
- See also: javascript-language-overview, javascript-dom