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

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 with pagehide).
  • resize, scroll.
  • online, offline.
  • error, unhandledrejection.
  • popstate, hashchange — navigation.
  • storage — fires in other tabs when localStorage changes.
  • messagepostMessage from 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