浏览器Window底层操作全解

浏览器 Window 底层操作全解

涵盖范围 :页面生命周期控制、窗口间通信、历史栈管理、存储机制、底层事件拦截等核心方向

难度梯度:⭐ 基础 API → ⭐⭐ 进阶用法 → ⭐⭐⭐ 深入原理 → ⭐⭐⭐⭐ 工程实践


📑 目录

  1. 页面关闭拦截与生命周期
  2. 窗口间数据共享与通信
  3. 窗口管理与导航控制
  4. 浏览器历史栈操作
  5. 页面可见性与后台运行
  6. 存储机制底层原理
  7. 窗口尺寸与视口控制
  8. 打印与媒体控制
  9. 底层事件与系统交互
  10. 安全策略与跨域限制

一、页面关闭拦截与生命周期

⭐ Q1:如何阻止用户关闭页面或刷新时丢失未保存的数据?

答:

浏览器提供了 beforeunload 事件来拦截页面关闭/刷新/跳转行为。

javascript 复制代码
// 基础用法:阻止页面关闭
window.addEventListener('beforeunload', (event) => {
  if (hasUnsavedChanges) {
    // 现代浏览器已不支持自定义消息,只能触发默认提示
    event.preventDefault();
    event.returnValue = ''; // Chrome 需要设置 returnValue
    return ''; // 某些旧浏览器需要 return
  }
});

⚠️ 重要限制(现代浏览器安全策略):

浏览器 行为
Chrome 51+ 忽略自定义消息,显示固定提示
Firefox 44+ 同上
Safari 同上
触发条件 必须用户有交互(点击、输入等),否则不弹窗
javascript 复制代码
// 推荐:配合表单自动保存
let hasUnsavedChanges = false;

// 监听输入变化
form.addEventListener('input', () => {
  hasUnsavedChanges = true;
});

// 自动保存到 sessionStorage(兜底)
form.addEventListener('input', debounce(() => {
  sessionStorage.setItem('draft', JSON.stringify(getFormData()));
  hasUnsavedChanges = false;
}, 1000));

window.addEventListener('beforeunload', (e) => {
  if (hasUnsavedChanges) {
    e.preventDefault();
    e.returnValue = '';
  }
});

⭐⭐ Q2:beforeunloadunloadpagehide 三个事件有什么区别?

答:

事件 触发时机 能否阻止关闭 能否执行异步操作 典型用途
beforeunload 页面即将卸载前 ✅ 可以弹窗阻止 ❌ 不建议 未保存数据提示
unload 页面正在卸载 ❌ 不能 ❌ 不可靠(同步代码可能不执行) 发送埋点(不推荐)
pagehide 页面隐藏(包括缓存) ❌ 不能 ⚠️ 有限 保存状态到 sessionStorage
visibilitychange 标签页可见性变化 ❌ 不能 ✅ 可以 暂停视频/轮询、保存草稿
javascript 复制代码
// 最佳实践:组合使用
// 1. 阻止意外关闭
window.addEventListener('beforeunload', (e) => {
  if (hasUnsavedChanges) {
    e.preventDefault();
    e.returnValue = '';
  }
});

// 2. 页面隐藏时保存状态(比 unload 更可靠)
window.addEventListener('pagehide', () => {
  sessionStorage.setItem('lastState', JSON.stringify(appState));
});

// 3. 页面重新可见时恢复
window.addEventListener('pageshow', (e) => {
  if (e.persisted) { // 从 bfcache 恢复
    console.log('页面从缓存恢复');
  }
});

⭐⭐⭐ Q3:什么是 bfcache(往返缓存)?如何控制它?

答:

bfcache(Back-Forward Cache) 是浏览器的一种优化机制:当用户点击"后退"时,直接从内存中恢复整个页面状态(包括 DOM、JS 堆内存),而不是重新加载。

css 复制代码
正常流程:后退 → 重新请求 HTML → 解析 → 执行 JS → 渲染
bfcache:后退 → 从内存恢复完整页面状态(瞬间完成)

触发 bfcache 的条件

  • 页面没有未完成的网络请求(XHR/fetch)
  • 没有打开的 WebSocket 连接
  • 页面没有使用 unload 事件监听器(Chrome 限制)
  • 没有正在播放的音频/视频
javascript 复制代码
// 检测是否从 bfcache 恢复
window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('从 bfcache 恢复,无需重新初始化');
    // 注意:此时页面状态是离开时的快照
    // 如果需要刷新数据,在这里处理
  }
});

// 主动阻止 bfcache(不推荐,除非必要)
window.addEventListener('unload', () => {}); // Chrome 会因此禁用 bfcache

最佳实践

  • 使用 pagehide 代替 unload(不阻止 bfcache)
  • 从 bfcache 恢复时,检查数据时效性,必要时重新获取

⭐⭐⭐⭐ Q4:如何实现一个可靠的"页面意外关闭前自动保存"机制?

答:

javascript 复制代码
class PageGuard {
  constructor() {
    this.draftKey = 'page_draft';
    this.lastSave = 0;
    this.init();
  }

  init() {
    // 1. 定时自动保存(每 5 秒)
    this.autoSaveTimer = setInterval(() => this.save(), 5000);

    // 2. 输入时防抖保存
    document.addEventListener('input', debounce(() => this.save(), 500));

    // 3. 页面隐藏时立即保存(最可靠)
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.save();
        // 发送 Beacon(页面关闭时仍可发送)
        navigator.sendBeacon('/api/log', JSON.stringify({ action: 'page_hide' }));
      }
    });

    // 4. 阻止关闭提示
    window.addEventListener('beforeunload', (e) => {
      if (this.hasUnsavedChanges()) {
        this.save(); // 最后尝试保存
        e.preventDefault();
        e.returnValue = '';
      }
    });
  }

  save() {
    const data = this.collectData();
    const payload = JSON.stringify({ data, timestamp: Date.now() });

    // 优先使用 IndexedDB(容量大)
    this.saveToIndexedDB(payload).catch(() => {
      // 降级到 sessionStorage
      sessionStorage.setItem(this.draftKey, payload);
    });

    this.lastSave = Date.now();
  }

  async saveToIndexedDB(payload) {
    // 使用 IndexedDB 存储大体积草稿
    const db = await openDB('DraftDB', 1);
    await db.put('drafts', payload, 'current');
  }

  collectData() {
    // 收集表单、编辑器状态等
    return { /* ... */ };
  }

  hasUnsavedChanges() {
    return Date.now() - this.lastSave > 1000; // 1秒内未保存视为有变更
  }

  restore() {
    const saved = sessionStorage.getItem(this.draftKey);
    if (saved) {
      const { data, timestamp } = JSON.parse(saved);
      // 恢复数据逻辑
      console.log('恢复草稿,保存时间:', new Date(timestamp));
    }
  }
}

二、窗口间数据共享与通信

⭐ Q1:如何在新打开的窗口和原窗口之间传递数据?

答:

方法一:window.open + opener(同域)

javascript 复制代码
// 父窗口
const child = window.open('https://same-domain.com/child.html', 'childWindow');

// 向子窗口发送数据(需同域)
child.onload = () => {
  child.postMessage('hello from parent', '*');
};

// 子窗口获取父窗口引用
console.log(window.opener); // 父窗口的 window 对象
window.opener.postMessage('hello from child', '*');

方法二:localStorage(同源跨标签页)

javascript 复制代码
// 窗口 A:写入数据
localStorage.setItem('crossTabData', JSON.stringify({ userId: 123, time: Date.now() }));

// 窗口 B:监听变化
window.addEventListener('storage', (e) => {
  if (e.key === 'crossTabData') {
    const data = JSON.parse(e.newValue);
    console.log('收到跨标签页数据:', data);
  }
});

⚠️ storage 事件特点

  • 只在其他标签页触发,当前页面修改不会触发自身事件
  • 仅在同源页面间生效
  • 只能监听 localStoragesessionStorage 不触发

⭐⭐ Q2:BroadcastChannel 是什么?比 localStorage 好在哪里?

答:

javascript 复制代码
// 创建广播频道(同源跨标签页/iframe)
const channel = new BroadcastChannel('app_channel');

// 发送消息
channel.postMessage({ type: 'LOGIN', userId: 123 });

// 接收消息
channel.onmessage = (event) => {
  console.log('收到广播:', event.data);
};

// 关闭频道
channel.close();
特性 localStorage + storage 事件 BroadcastChannel
API 设计 间接(通过存储变更) 直接的消息 API
数据类型 只能存字符串 支持结构化克隆(对象、数组、Blob 等)
性能 写入磁盘,较慢 内存通信,更快
隐私模式 可能不可用 可用
兼容性 全浏览器 Chrome 54+, Firefox 38+, Safari 15.4+

⭐⭐⭐ Q3:如何实现"获取前一个窗口的数据"?有哪些方案?

答:

场景:用户从页面 A 跳转到页面 B,页面 B 需要获取页面 A 的状态。

方案对比

方案 原理 优点 缺点
URL 参数 ?data=xxx 简单、可分享 数据量受限、暴露敏感信息
sessionStorage 跳转前写入,跳转后读取 不暴露 URL、容量大(5MB) 只能同源、页面关闭即清除
localStorage 同上,但持久化 跨会话可用 需手动清理、多标签冲突
BroadcastChannel 跳转前广播,新页监听 实时、结构化数据 需要短暂时间窗口
postMessage + opener 新页读取 window.opener 可跨域(配合 postMessage) 仅限 window.open 打开
SharedWorker 共享 Worker 存储状态 多标签共享内存 兼容性差、复杂
Service Worker + Cache SW 作为中间层 离线可用 实现复杂
javascript 复制代码
// ===== 推荐方案:sessionStorage(最可靠)=====
// 页面 A:跳转前保存
function navigateToB() {
  sessionStorage.setItem('prevPageData', JSON.stringify({
    from: 'pageA',
    selectedItems: [1, 2, 3],
    timestamp: Date.now()
  }));
  location.href = '/page-b';
}

// 页面 B:加载时读取
window.addEventListener('DOMContentLoaded', () => {
  const prevData = sessionStorage.getItem('prevPageData');
  if (prevData) {
    const data = JSON.parse(prevData);
    console.log('来自上一页的数据:', data);
    // 使用后清理
    sessionStorage.removeItem('prevPageData');
  }
});

// ===== 方案二:BroadcastChannel(实时同步)=====
// 页面 A
const channel = new BroadcastChannel('page_transition');
channel.postMessage({ source: 'pageA', payload: { userId: 123 } });

// 页面 B(在新标签页打开时立即能收到)
const channel = new BroadcastChannel('page_transition');
channel.onmessage = (e) => {
  if (e.data.source === 'pageA') {
    console.log('收到上一页数据:', e.data.payload);
  }
};

// ===== 方案三:URL + History State(配合路由)=====
// 页面 A
history.pushState({ from: 'pageA', data: { id: 1 } }, '', '/page-b');
// 页面 B
console.log(history.state); // { from: 'pageA', data: { id: 1 } }

⭐⭐⭐⭐ Q4:如何实现多标签页间的"单点登录"状态同步?

答:

javascript 复制代码
class CrossTabAuth {
  constructor() {
    this.channel = new BroadcastChannel('auth_channel');
    this.init();
  }

  init() {
    // 监听其他标签页的登录状态变化
    this.channel.onmessage = (event) => {
      switch (event.data.type) {
        case 'LOGIN':
          this.syncLogin(event.data.token);
          break;
        case 'LOGOUT':
          this.syncLogout();
          break;
        case 'TOKEN_REFRESH':
          this.updateToken(event.data.token);
          break;
      }
    };

    // 监听 storage(兼容旧浏览器)
    window.addEventListener('storage', (e) => {
      if (e.key === 'auth_event') {
        const event = JSON.parse(e.newValue);
        this.handleAuthEvent(event);
      }
    });
  }

  login(token) {
    localStorage.setItem('accessToken', token);
    // 广播给其他标签页
    this.channel.postMessage({ type: 'LOGIN', token });
    this.broadcastStorage({ type: 'LOGIN', token });
  }

  logout() {
    localStorage.removeItem('accessToken');
    this.channel.postMessage({ type: 'LOGOUT' });
    this.broadcastStorage({ type: 'LOGOUT' });
    window.location.href = '/login';
  }

  syncLogin(token) {
    // 其他标签页登录,本页自动同步
    localStorage.setItem('accessToken', token);
    console.log('检测到其他标签页登录,已同步状态');
  }

  syncLogout() {
    // 其他标签页登出,本页同步登出
    localStorage.removeItem('accessToken');
    alert('您的账号已在其他窗口登出');
    window.location.href = '/login';
  }

  broadcastStorage(data) {
    // 降级方案:通过 localStorage 触发 storage 事件
    localStorage.setItem('auth_event', JSON.stringify({
      ...data,
      timestamp: Date.now()
    }));
  }
}

// 使用
const auth = new CrossTabAuth();

三、窗口管理与导航控制

⭐ Q1:window.open 有哪些常用参数和注意事项?

答:

javascript 复制代码
// 完整参数
const win = window.open(
  'https://example.com',  // URL
  'windowName',           // 窗口名称(同名将复用窗口)
  'width=800,height=600,left=100,top=100,resizable=yes,scrollbars=yes'
);

// 常用窗口特性
const features = [
  'width=800',        // 窗口宽度
  'height=600',       // 窗口高度
  'left=100',         // 距离屏幕左边缘
  'top=100',          // 距离屏幕上边缘
  'resizable=yes',    // 可调整大小
  'scrollbars=yes',   // 显示滚动条
  'status=yes',       // 显示状态栏
  'toolbar=no',       // 隐藏工具栏
  'menubar=no',       // 隐藏菜单栏
  'location=no',      // 隐藏地址栏
  'noopener=yes',     // 新窗口不持有 opener 引用(安全)
  'noreferrer=yes'    // 不发送 Referer 头(安全)
].join(',');

安全最佳实践

javascript 复制代码
// 1. 始终使用noopener防止tabnabbing攻击
const win = window.open(url, '_blank', 'noopener,noreferrer');

// 2. 检查弹窗是否被拦截
if (!win || win.closed || typeof win.closed === 'undefined') {
  alert('弹窗被浏览器拦截,请允许弹窗');
}

// 3. 关闭窗口前检查
if (win && !win.closed) {
  win.close();
}

⭐⭐ Q2:如何检测当前窗口是否被其他窗口通过 window.open 打开?

答:

javascript 复制代码
// 检测当前窗口是否为弹出窗口
if (window.opener) {
  console.log('本窗口由其他窗口打开');
  console.log('opener URL:', window.opener.location.href); // 同域可访问

  // 安全:检查 opener 来源
  if (window.opener.location.origin !== location.origin) {
    // 非同域,opener 可能被恶意篡改
    window.opener = null; // 断开引用,防止 tabnabbing
  }
}

// 检测是否在 iframe 中
if (window.self !== window.top) {
  console.log('本页面在 iframe 中');
  console.log('父窗口:', window.parent);
  console.log('顶层窗口:', window.top);
}

// 检测是否被嵌套多层
console.log('嵌套深度:', window.parent === window.top ? 1 : '多层');

⭐⭐⭐ Q3:如何实现"只允许单标签页登录"?

答:

javascript 复制代码
class SingleTabLock {
  constructor() {
    this.channel = new BroadcastChannel('tab_lock');
    this.tabId = Math.random().toString(36).slice(2);
    this.isMaster = false;
    this.init();
  }

  init() {
    // 询问当前是否有主标签页
    this.channel.postMessage({ type: 'WHO_IS_MASTER' });

    this.channel.onmessage = (e) => {
      if (e.data.tabId === this.tabId) return;

      switch (e.data.type) {
        case 'WHO_IS_MASTER':
          if (this.isMaster) {
            this.channel.postMessage({ type: 'I_AM_MASTER', tabId: this.tabId });
          }
          break;
        case 'I_AM_MASTER':
          this.handleMasterExists(e.data.tabId);
          break;
        case 'CLAIM_MASTER':
          if (this.isMaster && e.data.tabId !== this.tabId) {
            // 有冲突,比较时间戳
            this.resolveConflict(e.data);
          }
          break;
      }
    };

    // 如果没有收到响应,自己成为主标签页
    setTimeout(() => {
      if (!this.isMaster) {
        this.claimMaster();
      }
    }, 500);

    // 页面关闭时释放锁
    window.addEventListener('beforeunload', () => {
      if (this.isMaster) {
        this.channel.postMessage({ type: 'MASTER_LEAVING' });
      }
    });
  }

  claimMaster() {
    this.isMaster = true;
    localStorage.setItem('master_tab_id', this.tabId);
    console.log('本页成为主标签页');
  }

  handleMasterExists(masterId) {
    console.log('已有主标签页:', masterId);
    // 显示提示或禁用某些功能
    document.body.classList.add('slave-tab');
  }

  resolveConflict(other) {
    // 简单策略:保持现状,或根据时间戳决定
  }
}

四、浏览器历史栈操作

⭐ Q1:history.pushStatehistory.replaceState 的区别?

答:

特性 pushState replaceState
历史记录 新增一条记录 替换当前记录
后退行为 可后退到上一页 不能后退(当前页被替换)
典型场景 路由跳转、分页 登录后替换、修改 URL 参数
javascript 复制代码
// pushState:新增历史记录
history.pushState({ page: 2 }, '', '/page/2');
// 用户点击后退 → 回到 /page/1

// replaceState:替换当前记录
history.replaceState({ page: 1 }, '', '/page/1');
// 用户点击后退 → 回到更前一页(跳过当前页)

⚠️ 重要pushState/replaceState 不会触发页面加载,需要手动处理内容更新。


⭐⭐ Q2:如何监听浏览器的前进/后退按钮?

答:

javascript 复制代码
// popstate 事件:监听前进/后退
window.addEventListener('popstate', (event) => {
  console.log('用户点击了前进或后退');
  console.log('当前状态:', event.state); // pushState 传入的 state 对象

  // 根据 state 恢复页面内容
  if (event.state?.page) {
    loadPage(event.state.page);
  }
});

// 注意:pushState/replaceState 不会触发 popstate!
history.pushState({ page: 2 }, '', '/page/2');
// 不会触发 popstate

完整的路由管理封装

javascript 复制代码
class HistoryRouter {
  constructor() {
    this.routes = {};
    window.addEventListener('popstate', (e) => this.handleChange(e.state));
  }

  register(path, handler) {
    this.routes[path] = handler;
  }

  push(path, state = {}) {
    history.pushState(state, '', path);
    this.handleChange(state);
  }

  replace(path, state = {}) {
    history.replaceState(state, '', path);
    this.handleChange(state);
  }

  back() {
    history.back();
  }

  handleChange(state) {
    const path = location.pathname;
    const handler = this.routes[path] || this.routes['/404'];
    handler?.(state);
  }
}

⭐⭐⭐ Q3:如何实现"浏览器前进/后退时恢复页面滚动位置"?

答:

javascript 复制代码
// 保存滚动位置
const scrollPositions = new Map();

// 页面滚动时记录位置(防抖)
let scrollTimer;
window.addEventListener('scroll', () => {
  clearTimeout(scrollTimer);
  scrollTimer = setTimeout(() => {
    const key = location.pathname + location.search;
    scrollPositions.set(key, { x: window.scrollX, y: window.scrollY });

    // 同时存入 history.state
    history.replaceState(
      { ...history.state, scrollY: window.scrollY },
      ''
    );
  }, 100);
});

// 前进/后退时恢复
window.addEventListener('popstate', () => {
  const scrollY = history.state?.scrollY || 0;
  window.scrollTo(0, scrollY);
});

// 或者使用原生 scrollRestoration(推荐)
if ('scrollRestoration' in history) {
  history.scrollRestoration = 'manual'; // 关闭浏览器自动恢复
  // 或 history.scrollRestoration = 'auto'; // 让浏览器自动处理
}

五、页面可见性与后台运行

⭐ Q1:document.visibilityState 有哪些状态?如何使用?

答:

javascript 复制代码
// visibilityState 有三个值:
// 'visible'   → 页面至少部分可见
// 'hidden'    → 页面完全不可见(最小化、切换标签)
// 'prerender' → 页面正在预渲染(部分浏览器支持)

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    console.log('页面进入后台');
    // 暂停轮询、视频、动画
    stopPolling();
    video.pause();
  } else if (document.visibilityState === 'visible') {
    console.log('页面回到前台');
    // 恢复轮询、视频
    startPolling();
    video.play();
  }
});

// 实用场景:统计页面真实停留时间
let visibleTime = 0;
let lastVisibleTime = Date.now();

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    lastVisibleTime = Date.now();
  } else {
    visibleTime += Date.now() - lastVisibleTime;
  }
});

⭐⭐ Q2:页面进入后台后,setIntervalsetTimeout 会有什么变化?

答:

定时器类型 后台行为 解决方案
setTimeout 延迟时间可能变长(浏览器节流) 使用 Web Worker 保持计时
setInterval 间隔可能变长,甚至被暂停 同上
requestAnimationFrame 完全暂停 回到前台时补帧
javascript 复制代码
// 后台计时方案:Web Worker
// timer.worker.js
let timer;
self.onmessage = (e) => {
  if (e.data === 'start') {
    timer = setInterval(() => {
      self.postMessage('tick');
    }, 1000);
  } else if (e.data === 'stop') {
    clearInterval(timer);
  }
};

// 主页面
const worker = new Worker('timer.worker.js');
worker.postMessage('start');
worker.onmessage = () => {
  console.log('1秒过去了(即使在后台也准确)');
};

⭐⭐⭐ Q3:如何实现"用户离开页面一段时间后自动登出"?

答:

javascript 复制代码
class AutoLogout {
  constructor(timeout = 30 * 60 * 1000) { // 默认30分钟
    this.timeout = timeout;
    this.timer = null;
    this.lastActivity = Date.now();
    this.init();
  }

  init() {
    // 监听用户活动
    ['click', 'mousemove', 'keydown', 'scroll', 'touchstart'].forEach(event => {
      document.addEventListener(event, () => this.resetTimer(), true);
    });

    // 页面可见性变化
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        // 检查离开期间是否超时
        if (Date.now() - this.lastActivity > this.timeout) {
          this.logout();
        } else {
          this.resetTimer();
        }
      }
    });

    this.resetTimer();
  }

  resetTimer() {
    this.lastActivity = Date.now();
    clearTimeout(this.timer);
    this.timer = setTimeout(() => this.logout(), this.timeout);
  }

  logout() {
    alert('长时间未操作,已自动登出');
    // 清除 token 并跳转
    localStorage.removeItem('token');
    location.href = '/login';
  }
}

new AutoLogout(30 * 60 * 1000); // 30分钟无操作自动登出

六、存储机制底层原理

⭐ Q1:localStoragesessionStoragecookie 的区别?

答:

特性 localStorage sessionStorage cookie
容量 ~5-10 MB ~5-10 MB ~4 KB
生命周期 永久(需手动清除) 页面会话(关闭标签即清除) 可设置 Expires/Max-Age
作用域 同源窗口共享 同源同标签页 可设置 Domain/Path
服务端读取 ❌ 不能 ❌ 不能 ✅ 自动随请求发送
跨标签通信 ✅ storage 事件 ❌ 不触发事件
性能 同步读写,阻塞主线程 同步读写 每次请求携带,增加开销

⭐⭐ Q2:IndexedDB 适合什么场景?和 localStorage 相比有什么优势?

答:

javascript 复制代码
// IndexedDB 基础操作
const request = indexedDB.open('MyDatabase', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const store = db.createObjectStore('users', { keyPath: 'id' });
  store.createIndex('name', 'name', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;

  // 写入
  const tx = db.transaction('users', 'readwrite');
  const store = tx.objectStore('users');
  store.add({ id: 1, name: 'Alice', data: largeBlob });

  // 读取
  const getReq = store.get(1);
  getReq.onsuccess = () => console.log(getReq.result);
};
特性 localStorage IndexedDB
容量 ~5MB 理论上无上限(通常 50MB+,可请求更多)
数据类型 仅字符串 任意结构化数据(对象、Blob、ArrayBuffer)
查询能力 无(只能 key-value) 索引、范围查询、游标遍历
事务支持
异步 ❌ 同步(阻塞) ✅ 异步(不阻塞主线程)
适用场景 简单配置、token 离线应用、大量数据缓存、文件存储

⭐⭐⭐ Q3:Storage 事件为什么在当前页面修改时不触发?如何 workaround?

答:

javascript 复制代码
// Storage 事件的设计意图:只通知其他同源页面
localStorage.setItem('key', 'value');
// 当前页面不会触发 storage 事件!

// ===== Workaround 1:自定义事件 + Storage 事件组合 =====
class CrossTabStorage {
  constructor() {
    this.listeners = new Map();
    window.addEventListener('storage', (e) => this.notify(e.key, e.newValue));
  }

  setItem(key, value) {
    const oldValue = localStorage.getItem(key);
    localStorage.setItem(key, value);

    // 手动触发当前页面的监听器
    if (oldValue !== value) {
      this.notify(key, value);
    }
  }

  onChange(key, callback) {
    if (!this.listeners.has(key)) this.listeners.set(key, []);
    this.listeners.get(key).push(callback);
  }

  notify(key, value) {
    this.listeners.get(key)?.forEach(cb => cb(value));
  }
}

// ===== Workaround 2:使用 BroadcastChannel(推荐)=====
const channel = new BroadcastChannel('storage_sync');

function setItem(key, value) {
  localStorage.setItem(key, value);
  channel.postMessage({ key, value, source: 'self' });
}

channel.onmessage = (e) => {
  console.log('存储变化(包括当前页面):', e.data);
};

七、窗口尺寸与视口控制

⭐ Q1:如何获取窗口的各种尺寸信息?

答:

javascript 复制代码
// 屏幕尺寸(物理显示器)
screen.width;           // 屏幕总宽度
screen.height;          // 屏幕总高度
screen.availWidth;      // 可用宽度(排除任务栏)
screen.availHeight;     // 可用高度
screen.pixelRatio;      // DPR(设备像素比)

// 窗口尺寸(浏览器窗口)
window.innerWidth;      // 视口宽度(含滚动条)
window.innerHeight;     // 视口高度(含滚动条)
window.outerWidth;      // 窗口总宽度(含边框、工具栏)
window.outerHeight;     // 窗口总高度

// 文档尺寸
 document.documentElement.clientWidth;   // 视口宽度(不含滚动条)
 document.documentElement.clientHeight;  // 视口高度(不含滚动条)
 document.documentElement.scrollWidth;   // 文档总宽度
 document.documentElement.scrollHeight;  // 文档总高度

// 滚动位置
window.scrollX || window.pageXOffset;   // 水平滚动
window.scrollY || window.pageYOffset;   // 垂直滚动

⭐⭐ Q2:如何实现"进入全屏"和"退出全屏"?

答:

javascript 复制代码
// 进入全屏
function enterFullscreen(element = document.documentElement) {
  if (element.requestFullscreen) {
    element.requestFullscreen();
  } else if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen();
  } else if (element.msRequestFullscreen) {
    element.msRequestFullscreen();
  }
}

// 退出全屏
function exitFullscreen() {
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document.webkitExitFullscreen) {
    document.webkitExitFullscreen();
  } else if (document.msExitFullscreen) {
    document.msExitFullscreen();
  }
}

// 监听全屏变化
document.addEventListener('fullscreenchange', () => {
  if (document.fullscreenElement) {
    console.log('进入全屏');
  } else {
    console.log('退出全屏');
  }
});

// 检测当前是否全屏
const isFullscreen = !!document.fullscreenElement;

⭐⭐⭐ Q3:如何实现"监听元素进入视口"(懒加载)?

答:

javascript 复制代码
// 现代方案:IntersectionObserver(性能优于 scroll 监听)
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 元素进入视口
      const img = entry.target;
      img.src = img.dataset.src; // 加载真实图片
      observer.unobserve(img);   // 停止观察(一次性)
    }
  });
}, {
  root: null,           // 视口作为根
  rootMargin: '100px',  // 提前 100px 触发
  threshold: 0.1        // 元素可见 10% 时触发
});

// 观察所有懒加载图片
document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

// 兼容性降级(IE)
function fallbackLazyLoad() {
  const images = document.querySelectorAll('img[data-src]');
  images.forEach(img => {
    const rect = img.getBoundingClientRect();
    if (rect.top < window.innerHeight && rect.bottom > 0) {
      img.src = img.dataset.src;
    }
  });
}
window.addEventListener('scroll', throttle(fallbackLazyLoad, 200));

八、打印与媒体控制

⭐ Q1:如何实现打印特定区域?

答:

javascript 复制代码
// 方法 1:使用 window.print() + CSS 媒体查询
@media print {
  body * {
    display: none !important;  // 隐藏所有内容
  }
  #print-area, #print-area * {
    display: block !important; // 只显示打印区域
  }
}

// 触发打印
function printArea() {
  window.print();
}

// 方法 2:动态创建 iframe(不影响当前页面)
function printHTML(html) {
  const iframe = document.createElement('iframe');
  iframe.style.position = 'absolute';
  iframe.style.left = '-9999px';
  document.body.appendChild(iframe);

  iframe.contentDocument.write(html);
  iframe.contentDocument.close();

  iframe.contentWindow.focus();
  iframe.contentWindow.print();

  // 打印完成后移除
  iframe.contentWindow.addEventListener('afterprint', () => {
    document.body.removeChild(iframe);
  });
}

⭐⭐ Q2:如何检测用户是否正在使用"打印预览"?

答:

javascript 复制代码
// 监听打印事件
window.addEventListener('beforeprint', () => {
  console.log('用户打开了打印预览');
  // 可以在这里加载高分辨率图片、展开折叠内容等
});

window.addEventListener('afterprint', () => {
  console.log('用户关闭了打印预览或完成打印');
});

// 检测打印样式是否生效(通过 matchMedia)
const mediaQueryList = window.matchMedia('print');
mediaQueryList.addListener((mql) => {
  if (mql.matches) {
    console.log('打印媒体查询匹配');
  }
});

九、底层事件与系统交互

⭐ Q1:navigator.sendBeacon 是什么?有什么用?

答:

javascript 复制代码
// sendBeacon:在页面卸载时可靠地发送数据
window.addEventListener('unload', () => {
  // ❌ 不可靠:unload 中同步请求可能被浏览器取消
  // fetch('/api/log', { method: 'POST', body: data }); 
});

window.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    // ✅ 可靠:即使页面关闭也会发送
    navigator.sendBeacon('/api/log', JSON.stringify({
      page: location.href,
      duration: Date.now() - enterTime,
      timestamp: Date.now()
    }));
  }
});
特性 fetch/XHR sendBeacon
异步 ✅(但不可见)
页面关闭后 ❌ 可能失败 ✅ 浏览器保证发送
返回值 Promise/Response boolean(是否入队成功)
数据格式 任意 BodyInit(Blob、FormData、字符串等)
大小限制 ~64KB(各浏览器不同)

⭐⭐ Q2:如何复制内容到剪贴板?

答:

javascript 复制代码
// 现代 API:Clipboard API(需要 HTTPS + 用户交互触发)
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    console.log('复制成功');
  } catch (err) {
    console.error('复制失败:', err);
    // 降级方案
    fallbackCopy(text);
  }
}

// 降级方案(兼容旧浏览器)
function fallbackCopy(text) {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.style.position = 'fixed';
  textarea.style.opacity = '0';
  document.body.appendChild(textarea);

  textarea.select();
  document.execCommand('copy'); // 已废弃但仍有兼容性
  document.body.removeChild(textarea);
}

// 读取剪贴板
async function readClipboard() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('剪贴板内容:', text);
  } catch (err) {
    console.error('读取失败(需要用户授权):', err);
  }
}

⭐⭐⭐ Q3:如何实现"页面离开前发送埋点数据"?

答:

javascript 复制代码
class PageTracker {
  constructor() {
    this.events = [];
    this.init();
  }

  init() {
    // 1. 定期批量发送
    setInterval(() => this.flush(), 5000);

    // 2. 页面隐藏时立即发送(最可靠)
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.flush(true); // true = 使用 sendBeacon
      }
    });

    // 3. beforeunload 兜底
    window.addEventListener('beforeunload', () => {
      this.flush(true);
    });
  }

  track(event) {
    this.events.push({
      ...event,
      timestamp: Date.now(),
      url: location.href
    });

    // 事件过多时立即发送
    if (this.events.length >= 10) {
      this.flush();
    }
  }

  flush(useBeacon = false) {
    if (this.events.length === 0) return;

    const payload = JSON.stringify(this.events);
    this.events = [];

    if (useBeacon && navigator.sendBeacon) {
      navigator.sendBeacon('/api/track', payload);
    } else {
      fetch('/api/track', {
        method: 'POST',
        body: payload,
        keepalive: true // 页面关闭后仍尝试发送(部分浏览器支持)
      }).catch(() => {
        // 发送失败,存回队列(使用 IndexedDB 持久化)
        this.saveFailed(payload);
      });
    }
  }

  async saveFailed(payload) {
    // 存入 IndexedDB,下次页面打开时重试
    const db = await openDB('TrackerDB', 1);
    await db.add('failed', { payload, time: Date.now() });
  }
}

十、安全策略与跨域限制

⭐ Q1:window.opener 有什么安全风险?如何防护?

答:

Tabnabbing 攻击 :恶意页面通过 window.opener 篡改原页面的 location

javascript 复制代码
// 攻击场景:
// 1. 用户在正常页面 A 点击链接打开恶意页面 B
// 2. 页面 B 通过 window.opener.location = '钓鱼网站' 篡改页面 A
// 3. 用户回到页面 A 时,看到的是钓鱼网站

// 防护方案 1:使用 noopener
<a href="https://untrusted.com" target="_blank" rel="noopener noreferrer">
  外部链接
</a>

// 防护方案 2:JS 打开窗口时
const win = window.open('https://untrusted.com', '_blank', 'noopener,noreferrer');

// 防护方案 3:如果必须保留 opener,限制其能力
if (window.opener) {
  window.opener = null; // 主动断开引用
}

⭐⭐ Q2:postMessage 跨窗口通信时如何保证安全?

答:

javascript 复制代码
// ❌ 不安全的写法
window.postMessage(data, '*'); // 允许任何来源接收

// ✅ 安全的写法
// 发送方:明确指定目标 origin
otherWindow.postMessage(data, 'https://trusted-domain.com');

// 接收方:严格验证来源
window.addEventListener('message', (event) => {
  // 1. 验证来源
  if (event.origin !== 'https://trusted-domain.com') {
    return;
  }

  // 2. 验证发送窗口(如果是从特定窗口发送)
  if (event.source !== expectedWindow) {
    return;
  }

  // 3. 验证数据格式(防止注入)
  if (!isValidMessage(event.data)) {
    return;
  }

  console.log('安全接收:', event.data);
});

function isValidMessage(data) {
  return typeof data === 'object' && 
         data.type && 
         typeof data.type === 'string';
}

⭐⭐⭐ Q3:同源策略对 window 操作有哪些限制?

答:

操作 同源 跨域
window.location 读取 ✅(只能读取写入后的值)
window.location 写入
window.document 读取
window.document 写入
window.opener 访问 有限(可设置 location)
postMessage ✅(需指定 origin)
localStorage
sessionStorage
IndexedDB
cookies 受 Domain/Path 限制
window.parent 有限
window.frames 有限

🎯 面试速查表

知识点 核心要点
阻止关闭 beforeunload + event.preventDefault() + event.returnValue = ''
页面生命周期 beforeunloadvisibilitychangepagehideunload
bfcache pagehide 替代 unloadpageshow 检测 event.persisted
跨标签通信 BroadcastChannel(首选)→ localStorage + storagepostMessage
获取前窗口数据 sessionStorage(最可靠)→ BroadcastChannelhistory.state
单标签登录 BroadcastChannel 广播 + localStorage 降级
历史栈 pushState(新增)/ replaceState(替换)+ popstate 监听
后台计时 Web Worker 保持 setInterval 准确性
自动登出 监听用户活动 + visibilitychange + 定时器
存储选择 localStorage(简单配置)→ IndexedDB(大数据/结构化)→ cookie(服务端读取)
打印控制 window.print() + @media print CSS
埋点发送 navigator.sendBeacon(页面关闭时可靠)
剪贴板 navigator.clipboard.writeText() + 降级 execCommand
安全 noopener 防 tabnabbing,postMessage 严格验证 origin

📌 面试建议 :回答浏览器底层问题时,建议从 API 用法底层原理安全限制工程实践 四个层次展开,体现对浏览器机制的系统性理解。

相关推荐
ydyd202604211 小时前
设备OEE怎么提升?数据采集+分析优化的完整方案
java·服务器·前端
(╹◡╹)2 小时前
11.RK3588本地大模型内存评估优化
java·linux·前端
电气研究所2 小时前
Java + Redis 实现工业设备告警去抖,避免变频器故障重复推送
java·前端
AI视觉网奇2 小时前
cannot import name ‘model_urls‘ from ‘torchvision.models.resnet‘
linux·前端·javascript
Vuji2 小时前
深入 pi-agent 内核:完整追踪一次 CLI 交互的完整链路
前端·agent
zfoo-framework2 小时前
安装pi agent 支持duojie和deepseek
java·服务器·前端
Sca_杰2 小时前
微信客服API对接速通
javascript·node.js
l1m0_3 小时前
告别反复修改Prompt:AI生成React应用并落地开发的实战复盘
前端·react.js·ui·ai·设计
xexpertS3 小时前
前端工程转型实践:从 Ember 迁移到 React,提升构建速度与研发效能
前端·react.js·前端框架