你做了一个“旅行准备”页面:地址栏里要保留当前选中的城市,刷新后仍记得深色主题,用户可以导入一份本地行程文件,还能在主动授权后查看当前位置。若页面里嵌入了地图组件,主页面与 iframe 之间还要安全地交换消息。
这些需求看起来分散,实际上都属于浏览器提供给网页的能力。JavaScript 语言负责表达逻辑;浏览器再通过 window、location、history、navigator、Web Storage、FileReader 等对象,把标签页、地址栏、设备与本地数据连接起来。
本章会把这些 API 放回真实任务中。重点不只是“方法叫什么”,还包括它何时会失败、需要什么权限,以及哪些数据绝不能随手保存。
本章代码面向现代浏览器页面。请通过本地开发服务器运行示例;Node.js 没有浏览器标签页,因此默认不存在同一种 window、document、location 或 localStorage。
完成本章后,你应该能够:
window、DOM 与 BOM 的关系,并避免污染全局命名空间;URL 与 location 可靠地解析、修改和跳转地址;localStorage 与 sessionStorage 之间做出合适选择;postMessage() 在 iframe 或新窗口之间建立受约束的通信。
window 是当前浏览上下文的总入口,但它并不等于 JavaScript 语言本身。
打开一个页面时,浏览器会创建一个浏览上下文。顶层页面、每个 iframe、由脚本打开的新窗口,都有各自的全局对象;在普通网页脚本中,这个对象通常就是 window。
很多常用名字都可以从它上面找到:
window.document;
window.location;
window.history;
window.navigator;
window.localStorage;
window.setTimeout;因此,下面两次调用在浏览器页面中使用的是同一个定时器能力:
setTimeout(showTip, 1000);
window.setTimeout(showTip, 1000);document 是 DOM 的入口,负责描述和修改页面文档树。BOM 则是一个常用统称,用来描述地址栏、会话历史、浏览器信息、窗口尺寸、定时器等“文档之外”的浏览器能力。它不是一套独立于浏览器规范的 JavaScript 语法。
打开浏览器开发者工具的 Console,执行:
console.dir(window);你可以展开对象,找到 document、location、history、navigator 和 innerWidth。再做几个不会修改页面的观察:
console.log(window === globalThis);
console.log(window.document === document);
console.log(window.location.href);
console.log(window.innerWidth, window.innerHeight);在普通网页脚本的顶层,globalThis 指向 window。使用 globalThis 的好处是,它也能代表 Worker 或 Node.js 等环境各自的全局对象;但其他环境的全局对象不一定叫 window。
传统非模块脚本中的顶层 var 和函数声明,通常会成为 window 的属性:
<script>
var currentUser = "小虎";
function renderUser() {
document.body.textContent = currentUser;
}
console.log(window.currentUser); // 小虎
</script>顶层 let、const 不会以同样方式创建 window 属性,模块脚本也拥有自己的模块作用域。不过,真正稳妥的做法仍是把功能放进模块或函数,只公开必要入口,避免不同脚本覆盖同名变量。
window.location 是当前页面的 Location 对象。它看起来像一个地址字符串,但实际上还提供了 URL 分段与导航方法。
假设页面地址是:
https://learn.example:8443/course/window?chapter=14&mode=lab#timers常见属性如下:
URL 与 URLSearchParams 更适合读取和修改地址:
const url = new URL(
"https://learn.example:8443/course/window?chapter=14&mode=lab#timers"
);
console.log(url.pathname); // /course/window
console.log(url.searchParams.get("chapter")); // 14
url.searchParams.set("mode", "review");
url.hash = "storage";
console.log(url.href);
// https://learn.example:8443/course/window?chapter=14&mode=review#storage相对地址必须有基准地址。浏览器页面里可以写:
const detailUrl = new URL("./detail?id=42", location.href);
console.log(detailUrl.href);URLSearchParams.get() 找不到键时返回 null;同名参数可以出现多次,需要全部读取时使用 getAll()。
这三个方法都会影响当前页面:
location.assign("/login");
location.replace("/login");
location.reload();给 location.href 赋值也会导航,效果更接近 assign()。练习这些方法时要保存好未提交内容,因为调用后当前文档会离开或刷新。
如果跳转目标来自查询参数,不能直接照单全收:
function safeRedirect(rawTarget) {
const target = new URL(rawTarget, location.origin);
if (target.origin !== location.origin) {
throw new Error("只允许站内跳转");
}
location.assign(target.href);
}这段代码把相对地址解析到当前源,并拒绝外部 origin,避免页面变成任意跳转入口。真实项目还可以进一步限制允许的路径。
history 管理当前标签页或 frame 的会话历史。它可以移动历史位置,也可以在不重新加载整页的前提下更新当前地址。

location 关注当前地址;history 关注这个浏览上下文走过的会话路径。
基本移动方法很直观:
history.back(); // 大致等价于 go(-1)
history.forward(); // 大致等价于 go(1)
history.go(-2); // 向后移动两项浏览器不会把完整历史 URL 列表交给网页。脚本可以读取 history.length、移动位置、读取当前项的 history.state,却不能枚举用户访问过的所有地址。
单页界面切换筛选条件时,可以同步改变地址栏:
function showView(view) {
document.querySelector("#view").textContent = `当前视图:${view}`;
}
function navigateTo(view) {
const nextUrl = new URL(location.href);
nextUrl.searchParams.set("view", view);
history.pushState({ view }, "", nextUrl);
showView(view);
}
pushState(state, "", url) 新增一项;replaceState() 修改当前项,不增加长度。传入的 URL 必须与当前文档同源,state 也必须是可结构化克隆的数据,不能把 DOM 节点或函数塞进去。
还有一个很容易漏掉的规则:调用 pushState() 或 replaceState() 不会自动触发 popstate,也不会自动更新 DOM。代码必须主动渲染;用户随后前进、后退到不同历史项时,浏览器才会触发 popstate。
navigator 汇集了当前浏览器环境暴露的一些信息和能力。判断功能是否可用时,优先做能力检测,不要根据浏览器名称猜测:
if ("geolocation" in navigator) {
console.log("可以请求定位");
} else {
console.log("当前环境没有提供定位 API");
}
if ("clipboard" in navigator) {
console.log("检测到剪贴板入口");
}检测到属性只代表“存在入口”,不代表调用必定成功。安全上下文、用户权限、设备能力与浏览器策略仍可能拒绝请求。
navigator.userAgent 可用于排查兼容问题,但不适合决定关键业务流程。它可能被简化、冻结或伪装;“检测能力并提供降级方案”通常更可靠。
定位是敏感能力。调用通常需要 HTTPS 等安全上下文(localhost 常被作为开发例外处理),浏览器会让用户决定是否授权。
const output = document.querySelector("#position");
const locateButton = document.querySelector("#locate");
locateButton.addEventListener("click", () => {
if (!("geolocation" in navigator)) {
output.textContent = "当前浏览器不支持定位。";
return;
}
output.textContent = "正在请求位置…"
成功回调拿到的是 GeolocationPosition。其中 coords.latitude、coords.longitude 和 coords.accuracy 都是数值,accuracy 表示以米为单位的估计误差半径,不是“精确到小数点后几位”的保证。altitude、altitudeAccuracy、heading 和 speed 则可能为 null。
选项也有明确取舍:
enableHighAccuracy: true 可能更慢、更耗电,并不保证一定更准;timeout 限制本次等待时间;maximumAge 允许复用多旧的缓存位置,0 表示不接受缓存结果。持续跟踪位置要用 watchPosition(),并保存返回值,在不再需要时调用 clearWatch(id)。不要在页面一加载就索要定位;先解释用途,再由用户点击触发,并只保存完成任务所需的最少信息。
如果定位代码运行在 iframe 中,还要同时满足父页面的 Permissions Policy 与 iframe 的 allow 配置;页面自身获得授权,并不代表每个嵌入上下文都自动拥有定位能力。
页面常要“稍后执行”“每秒更新”或“窗口变化后重算”。这两类任务分别由定时器与视口 API 提供信息,但它们都不是精确的硬件承诺。
setTimeout() 安排一次回调,setInterval() 安排重复回调:
const timerId = window.setTimeout(() => {
console.log("至少等待了约 1000 毫秒");
}, 1000);
cancelButton.addEventListener("click", () => {
window.clearTimeout(timerId);
});
延迟结束后,回调只是获得了排队资格;主线程仍要先完成当前任务。
下面代码一定先输出 同步结束:
setTimeout(() => {
console.log("定时器回调");
}, 0);
console.log("同步结束");同步结束
定时器回调延迟可能因为长任务、后台标签页节流、设备负载和浏览器最小延迟而变长。重复任务也可能产生漂移。若下一次任务要等本次异步工作完成,更适合递归安排下一次 setTimeout():
let stopped = false;
async function poll() {
await refreshStatus();
if (!stopped) {
setTimeout(poll, 5000);
}
}
poll();组件卸载、页面功能关闭或任务取消时,要清理仍在等待的定时器。
重复更新时保存 setInterval() 返回的 id,并用对应的清理函数停止:
const clockId = setInterval(updateClock, 1000);
stopButton.addEventListener("click", () => {
clearInterval(clockId);
});
页面布局优先使用 CSS 媒体查询、容器查询和弹性布局。只有脚本确实需要参与时才监听尺寸,例如调整 canvas 像素或移动浮层。
const label = document.querySelector("#viewport-size");
let frameId = 0;
function renderViewportSize() {
label.textContent = `${window.innerWidth} × ${window.innerHeight}`;
frameId = 0;
}
window.addEventListener("resize", () => {
if
resize 可能高频触发,这里用 requestAnimationFrame() 把一帧内多次变化合并成一次渲染。移动端还可以检测 window.visualViewport,再监听它的 resize 与 scroll;该对象并非所有旧环境都具备,所以要先判断是否存在。
“保存在浏览器里”不是一种统一行为。cookie、localStorage 和 sessionStorage 的生命周期、作用范围与网络行为都不同。
Storage 提供 setItem()、getItem()、removeItem()、clear()、key() 与 length:
localStorage.setItem("theme", "dark");
console.log(localStorage.getItem("theme")); // dark
localStorage.removeItem("theme");
console.log(localStorage.getItem("theme")); // null对象必须先序列化:
const SETTINGS_KEY = "travel-settings";
function saveSettings(settings) {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
return true;
} catch (error) {
console.error("设置保存失败", error);
return false;
}
}
function loadSettings
try...catch 同时覆盖两类问题:JSON 可能损坏,存储也可能因配额、隐私设置或安全策略不可用。Web Storage 是同步 API,大量读写会阻塞主线程,所以不适合大型数据集。
其他同源页面修改 localStorage 时,当前页面可以监听 storage 事件来同步状态:
window.addEventListener("storage", (event) => {
if (event.storageArea !== localStorage) {
return;
}
if (event.key === null) {
// 另一个同源页面调用了 localStorage.clear()
applySettings({ theme: "light" });
return;
}
if (event.key !== SETTINGS_KEY) {
return;
}
if
监听器先用 event.storageArea 确认变化来自 localStorage,避免把 sessionStorage 的同名键或清空事件误当成持久设置。对于 localStorage.clear(),event.key 与 event.newValue 都是 null;removeItem() 删除目标键时,key 仍是该键名,但 newValue 为 null。触发写入的当前页面通常不会收到自己造成的这次 storage 事件;它应该在写入成功后直接更新自身界面。
document.cookie 的读取结果是一串以分号分隔的可读 cookie;给它赋值时,不是替换整个字符串,而是在当前作用域内设置或更新一条 cookie。
document.cookie =
`theme=${encodeURIComponent("dark")}; Max-Age=86400; Path=/; SameSite=Lax; Secure`;一个简单读取函数可以这样写:
function getCookie(name) {
const prefix = `${encodeURIComponent(name)}=`;
for (const part of document.cookie.split(";")) {
const cookie = part.trim();
if (cookie.startsWith(prefix)) {
return decodeURIComponent(cookie.slice(prefix.length));
常见属性的含义:
Max-Age 或 Expires 控制过期时间;Path 和 Domain 控制发送范围,Domain 通常应尽量收窄;Secure 让 cookie 只通过安全连接发送;SameSite 限制跨站请求携带,有助于降低部分跨站请求伪造风险;HttpOnly 禁止页面 JavaScript 读取,只能由服务端通过响应头设置。删除 cookie 的本质是使用相同的名称、Path 和 Domain,再把过期时间设为过去或把 Max-Age 设为 0。
同源脚本能够读取该 origin 下的 localStorage、sessionStorage 与非 HttpOnly cookie。如果页面发生 XSS,恶意脚本也拥有同样权限。因此:
HttpOnly、Secure 和合适的 SameSite;textContent,不要直接拼入 innerHTML;localStorage 解决的是“持久保存字符串”,不是“安全保存秘密”。安全性必须依赖最小化数据、服务端会话设计、内容输出编码和完善的 XSS 防护共同建立。
网页不能任意浏览用户硬盘。页面通常只能处理用户通过 <input type="file">、拖放等方式明确交给它的 File 对象。这条边界让“本地预览”成为可能,也避免网页默默扫描文件系统。
<label>
导入行程 JSON
<input id="file-input" type="file" accept="application/json,.json" />
</label>
<pre id="file-output">尚未选择文件</pre>用户选择后,input.files 是 FileList。每个 File 都有 name、size、type 和 lastModified 等属性:
const input = document.querySelector("#file-input");
input.addEventListener("change", () => {
const file = input.files?.[0];
if (!file) {
return;
}
console.log({
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
});
});accept 只是文件选择器的提示,不是安全验证。MIME type 也可能为空或不准确。页面可以先检查扩展名、声明类型和大小来改善体验;如果文件还要上传,服务端必须重新验证真实内容。
下面示例限制文件大小、读取文本、解析 JSON,并把结果安全地显示到 pre:
const input = document.querySelector("#file-input");
const output = document.querySelector("#file-output");
let activeReader = null;
input.addEventListener("change", () => {
const file = input.files?.[0];
if (!file) {
output.textContent = "没有选择文件。"
FileReader 的读取结果取决于方法:
读取过程是异步的,结果要在 load 后使用。开始新读取前可以调用 abort() 取消旧任务。
图片预览可以用 readAsDataURL():
function previewImage(file, imageElement) {
if (!file.type.startsWith("image/")) {
throw new Error("请选择图片文件");
}
const reader = new FileReader();
reader.addEventListener("load", () => {
imageElement.src = String(reader.result);
imageElement.alt =
对于大图片,仅做预览时,URL.createObjectURL(file) 通常比 data URL 更省内存;图片不用后要调用 URL.revokeObjectURL(url)。无论哪种方式,都不要用文件名或文件文本直接拼接 innerHTML。
顶层页面、iframe 和新窗口各自拥有 window。它们可以拿到彼此的引用,但能否直接读取 DOM 或变量,首先取决于同源策略。

协议、主机名与端口都相同才算同源;路径不同不影响同源判断。
同源 iframe 的父页面可以访问其 contentWindow 与文档:
const frame = document.querySelector("#preview-frame");
frame.addEventListener("load", () => {
// 仅当 frame 内容与父页面同源时才允许直接读取
console.log(frame.contentWindow.document.title);
});iframe 内部可以通过 window.parent 指向父上下文,window.top 指向最顶层上下文。跨源时,浏览器会阻止大多数直接读取操作。
sandbox 属性可以进一步限制 iframe 的脚本、表单、弹窗与 origin 能力。只添加真正需要的令牌;不要为了“让它能跑”一次性开放全部权限。
浏览器通常只允许在用户手势期间打开窗口:
openButton.addEventListener("click", () => {
const child = window.open(
"/help",
"help-window",
"popup,width=720,height=600"
);
if (!child) {
showMessage("窗口被浏览器拦截,请允许弹窗后重试。");
}
});打开不受信任的外部页面时,应切断 opener:
<a
href="https://example.org"
target="_blank"
rel="noopener noreferrer"
>
打开外部页面
</a>脚本也可以使用 window.open(url, "_blank", "noopener")。启用 noopener 后,调用方不要再依赖返回的窗口引用;不同浏览器行为可能表现为 null。
跨源页面确实需要协作时,发送方应把目标 origin 写完整:
const frame = document.querySelector("#map-frame");
const mapOrigin = "https://map.example";
frame.addEventListener("load", () => {
frame.contentWindow.postMessage(
{
type: "map:center",
payload: { latitude: 31.2304, longitude: 121.4737 }
},
mapOrigin
);
});接收方至少验证三件事:event.origin、event.source 和数据结构。
const allowedParentOrigin = "https://learn.example";
window.addEventListener("message", (event) => {
if (event.origin !== allowedParentOrigin) {
return;
}
if (event.source !== window.parent) {
return;
}
const message = event.data;
const latitude = message?.payload?.latitude;
const longitude
只有当接收方 origin 确实不可预知时,发送端才可能不得不用 "*";一般业务通信应使用精确的 targetOrigin。接收到的 event.data 仍属于外部输入,不能直接写入 innerHTML 或当作命令执行。
这次实践把地址、历史、存储、文件、定位与定时器放进一个可运行页面。请创建 index.html,通过本地开发服务器打开。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>浏览器能力面板</title>
<style>
:root {
color-scheme: light dark
新建 app.js:
const routeOutput = document.querySelector("#route-output");
const themeSelect = document.querySelector("#theme");
const fileInput = document.querySelector("#file-input");
const fileOutput = document.querySelector("#file-output");
const locateButton = document.querySelector("#locate");
const positionOutput
准备一个小于 100 KB 的测试文件:
{
"city": "上海",
"days": 3,
"notes": ["带伞", "提前订票"]
}然后逐项检查:
view 参数和页面文字同步变化;定位请在 HTTPS 或 localhost 上测试。实际经纬度、精度、权限提示和等待时间都由设备与浏览器决定,不应写成固定结果。
window 是当前浏览上下文的全局入口,document 负责 DOM,其他宿主环境不一定提供 window。URL 和 URLSearchParams 拆解地址;导航前验证不可信的跳转目标。pushState() 新增历史项,replaceState() 替换当前项;两者都不会自动渲染 DOM。FileReader 只能读取页面已经拿到的文件,读取结果在事件回调中处理,上传内容仍需服务端验证。postMessage() 必须限制目标 origin,并验证收到的 origin、窗口与数据结构。