综合场景面试题

#前端#综合场景

前端面试题 - 综合场景篇

本章节收录前端综合场景面试题,重点考察候选人的知识体系整合能力和实际问题解决能力。涵盖从URL输入到页面加载、性能优化实战、业务场景实现等高频面试题目。


📊 数据统计

统计项数值
总题数45道
基础题12道 (26.7%)
中级题23道 (51.1%)
高级题10道 (22.2%)

📚 目录


一、从URL到页面加载

1. 从输入URL到页面加载完成,经历了哪些过程?

难度:⭐⭐⭐(高级)
标签:#浏览器 #网络 #渲染 #高频

问题描述
详细描述用户在浏览器地址栏输入URL并按下回车,到页面完全展示出来的整个过程。

参考答案要点

整个过程可分为网络阶段解析阶段渲染阶段三个大阶段:

网络阶段

  1. URL解析

    • 协议解析(http/https)
    • 域名解析
    • 路径和参数解析
    • HSTS检查(强制HTTPS)
  2. DNS解析(域名→IP)

    • 浏览器缓存 → 系统缓存 → 路由器缓存 → ISP DNS → 根域名服务器 → 顶级域名服务器 → 权威域名服务器
    • DNS预解析优化:<link rel="dns-prefetch" href="//example.com">
  3. 建立TCP连接(三次握手)

    • 客户端发送SYN(seq=x)
    • 服务端返回SYN+ACK(seq=y, ack=x+1)
    • 客户端发送ACK(ack=y+1)
    • HTTPS还需TLS握手(交换加密算法、证书验证、密钥协商)
  4. 发送HTTP请求

    • 构建请求报文(请求行、请求头、请求体)
    • 发送请求到服务器
  5. 服务器处理并返回响应

    • 服务器接收请求
    • 后端处理(路由、数据库查询等)
    • 返回HTTP响应(状态码、响应头、响应体)

解析阶段

  1. 浏览器接收响应

    • 解析响应头(Content-Type、Content-Length等)
    • 根据Content-Type决定处理方式
  2. 解析HTML构建DOM树

    • 词法分析(Token化)
    • 语法分析(构建DOM树)
    • 遇到<script>会阻塞解析(除非有defer/async)
    • 遇到<link>会并行请求CSS
  3. 解析CSS构建CSSOM树

    • 解析CSS文件和<style>标签
    • 构建CSSOM树
    • CSS解析不会阻塞HTML解析,但会阻塞渲染

渲染阶段

  1. 构建渲染树(Render Tree)

    • 合并DOM树和CSSOM树
    • 排除display: none的节点
    • 计算每个节点的样式
  2. 布局(Layout/Reflow)

    • 计算每个元素的位置和大小
    • 生成布局树(Layout Tree)
  3. 绘制(Paint)

    • 将渲染树转换为屏幕上的像素
    • 分层绘制(背景、文字、边框等)
  4. 合成(Composite)

    • 将多个图层合成最终页面
    • GPU加速合成
  5. 断开连接(四次挥手,可选)

    • Connection: keep-alive保持长连接
    • 或四次挥手断开TCP连接

关键优化点

  • DNS预解析
  • TCP长连接
  • 资源预加载(preload/prefetch)
  • 减少阻塞资源
  • 启用HTTP/2或HTTP/3

2. DNS解析的详细过程是怎样的?

难度:⭐⭐(中级)
标签:#DNS #网络

问题描述
详细描述DNS解析的完整流程,包括缓存查询顺序和递归查询过程。

参考答案要点

DNS查询顺序(从近到远)

  1. 浏览器缓存(Chrome约1分钟,TTL控制)
  2. 操作系统缓存(hosts文件 + DNS客户端服务)
  3. 路由器缓存(本地DNS服务器)
  4. ISP DNS服务器(运营商DNS,如114.114.114.114)
  5. 根域名服务器(全球13组,返回顶级域名服务器地址)
  6. 顶级域名服务器(如.com、.cn,返回权威DNS服务器)
  7. 权威域名服务器(域名注册商提供,返回最终IP)

DNS解析类型

  • 递归查询:客户端→本地DNS→根→顶级→权威,层层返回
  • 迭代查询:本地DNS分别查询根、顶级、权威

DNS优化

HTML
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com" />

<!-- 预连接(DNS+TCP+TLS) -->
<link rel="preconnect" href="https://cdn.example.com" />

3. TCP三次握手和四次挥手的详细过程?

难度:⭐⭐⭐(高级)
标签:#TCP #网络

问题描述
详细解释TCP三次握手建立连接和四次挥手断开连接的过程,以及为什么是三次和四次。

参考答案要点

三次握手(建立连接)

Plain Text
客户端                    服务端
  |                         |
  |-------- SYN,seq=x ----->|  (同步序列号)
  |                         |
  |<-- SYN,seq=y,ACK=x+1 ---|  (同步+确认)
  |                         |
  |-------- ACK,y+1 ------->|  (确认)
  |                         |
  1. 第一次握手:客户端发送SYN包(seq=x),进入SYN_SENT状态
  2. 第二次握手:服务端收到SYN,发送SYN+ACK包(seq=y, ack=x+1),进入SYN_RECV状态
  3. 第三次握手:客户端收到SYN+ACK,发送ACK包(ack=y+1),双方进入ESTABLISHED状态

为什么是三次?

  • 防止历史重复连接初始化(已失效的连接请求报文突然到达)
  • 同步双方初始序列号
  • 确认双方收发能力正常

四次挥手(断开连接)

Plain Text
客户端                    服务端
  |                         |
  |-------- FIN ------------>|  (我要关闭)
  |                         |
  |<-------- ACK ------------|  (知道了)
  |                         |
  |<-------- FIN ------------|  (我也要关闭)
  |                         |
  |-------- ACK ------------>|  (知道了)
  |                         |
  1. 第一次挥手:主动方发送FIN包,进入FIN_WAIT_1状态
  2. 第二次挥手:被动方收到FIN,发送ACK包,进入CLOSE_WAIT状态
  3. 第三次挥手:被动方发送FIN包,进入LAST_ACK状态
  4. 第四次挥手:主动方收到FIN,发送ACK包,进入TIME_WAIT状态(等待2MSL后关闭)

为什么是四次?

  • TCP全双工通信,每个方向需要单独关闭
  • 被动方收到FIN后,可能还有数据要发送

TIME_WAIT作用

  • 确保最后一个ACK能被对方收到(可重传)
  • 防止已失效的连接请求报文出现在本连接中

4. HTTPS的握手过程是怎样的?

难度:⭐⭐⭐(高级)
标签:#HTTPS #TLS #安全

问题描述
详细描述HTTPS建立安全连接的TLS握手过程。

参考答案要点

TLS 1.2握手过程(2-RTT)

  1. Client Hello

    • 客户端支持的TLS版本
    • 支持的加密算法套件(Cipher Suites)
    • 客户端随机数(Client Random)
    • 支持的压缩方法
  2. Server Hello

    • 确认的TLS版本
    • 选定的加密算法套件
    • 服务端随机数(Server Random)
    • 服务器证书(含公钥)
    • 可选:请求客户端证书
  3. 客户端验证与密钥交换

    • 验证服务器证书(CA签名、有效期、域名匹配)
    • 生成预主密钥(Pre-Master Secret)
    • 用服务器公钥加密预主密钥发送
    • 发送Change Cipher Spec(之后加密通信)
    • 发送Finished消息(验证握手完整性)
  4. 服务端确认

    • 用私钥解密预主密钥
    • 发送Change Cipher Spec
    • 发送Finished消息
  5. 生成会话密钥

    • 客户端随机数 + 服务端随机数 + 预主密钥 → 主密钥
    • 主密钥派生出对称加密密钥、MAC密钥、IV

TLS 1.3优化(1-RTT或0-RTT)

  • 简化握手流程
  • 支持0-RTT恢复(基于PSK)
  • 移除过时算法

5. 浏览器如何解析和渲染页面?

难度:⭐⭐⭐(高级)
标签:#浏览器 #渲染 #DOM #CSSOM

问题描述
详细描述浏览器从接收HTML到渲染页面的完整过程。

参考答案要点

渲染流程

Plain Text
HTML → DOM Tree
         ↓
CSS → CSSOM Tree
         ↓
    Render Tree(渲染树)
         ↓
    Layout(布局/重排)
         ↓
    Paint(绘制/重绘)
         ↓
    Composite(合成)
  1. 构建DOM树

    • 词法分析:将HTML字符串转换为Token
    • 语法分析:根据Token构建DOM节点
    • 遇到<script>阻塞解析(除非defer/async)
    • 遇到<link>并行加载CSS
  2. 构建CSSOM树

    • 解析CSS文件和<style>标签
    • CSSOM是样式对象的树形结构
    • 样式计算(Computed Style)
  3. 构建渲染树

    • 合并DOM和CSSOM
    • 只包含可见节点(排除display: none
    • 计算每个节点的最终样式
  4. 布局(Layout/Reflow)

    • 计算每个元素的几何信息(位置、大小)
    • 生成布局树(Layout Tree)
    • 百分比、auto等值转为绝对像素
  5. 绘制(Paint)

    • 将渲染树转为屏幕像素
    • 分层绘制(背景、文字、边框等)
    • 生成绘制记录(Paint Records)
  6. 合成(Composite)

    • 将页面分层(Layer)
    • GPU合成图层(Composite Layers)
    • 最终显示到屏幕

关键概念

  • Reflow(重排):布局或几何属性变化,重新计算布局
  • Repaint(重绘):外观变化但不影响布局,重新绘制
  • Composite(合成):transform/opacity变化,直接合成

性能损耗:Reflow > Repaint > Composite


二、浏览器渲染原理

6. 什么是重排(Reflow)和重绘(Repaint)?如何优化?

难度:⭐⭐⭐(高级)
标签:#性能优化 #渲染 #重排 #重绘

问题描述
解释重排和重绘的区别,列举触发条件,并提供优化方案。

参考答案要点

重排(Reflow/Layout)

定义:当元素的几何属性(位置、大小)发生变化,浏览器需要重新计算布局。

触发条件

  • 修改元素尺寸(width/height/padding/margin)
  • 修改元素位置(top/left/right/bottom)
  • 改变显示属性(display: none/block)
  • 修改内容(文本、图片大小变化)
  • 修改字体大小
  • 激活CSS伪类(:hover)
  • 查询布局属性(offsetHeight/scrollTop/clientWidth等)
  • 浏览器窗口resize

影响范围

  • 局部重排:修改元素不影响其他元素布局
  • 全局重排:修改元素影响整个文档布局

重绘(Repaint/Paint)

定义:当元素的外观属性变化但不影响布局时,浏览器重新绘制元素。

触发条件

  • 修改颜色(color/background-color)
  • 修改背景(background-image/background-position)
  • 修改边框样式(border-color)
  • 修改阴影(box-shadow)
  • 修改透明度(opacity)
  • 修改文字装饰(text-decoration)

优化策略

  1. 减少Reflow次数

    JAVASCRIPT
    // ❌ 坏做法:多次触发Reflow
    const el = document.getElementById("box")
    el.style.width = "100px"
    console.log(el.offsetHeight) // 强制Reflow
    el.style.height = "200px"
    
    // ✅ 好做法:批量修改
    const el = document.getElementById("box")
    el.classList.add("new-size") // 一次性修改
  2. 使用DocumentFragment

    JAVASCRIPT
    // ✅ 批量DOM操作
    const fragment = document.createDocumentFragment()
    for (let i = 0; i < 1000; i++) {
      const li = document.createElement("li")
      fragment.appendChild(li)
    }
    ul.appendChild(fragment) // 只触发一次Reflow
  3. 离线修改

    JAVASCRIPT
    // ✅ 先隐藏,修改完再显示
    el.style.display = "none"
    // ... 大量修改 ...
    el.style.display = "block" // 只触发2次Reflow
  4. 使用transform和opacity

    CSS
    /* ✅ GPU加速,不触发Reflow/Repaint */
    .animated {
      transform: translateX(100px);
      opacity: 0.5;
      will-change: transform, opacity;
    }
  5. 避免table布局

    • table布局需要多次计算,任何单元格变化都会导致整个table重排
  6. 缓存布局属性

    JAVASCRIPT
    // ❌ 多次查询触发Reflow
    for (let i = 0; i < len; i++) {
      arr[i] = el.offsetHeight
    }
    
    // ✅ 缓存后使用
    const height = el.offsetHeight
    for (let i = 0; i < len; i++) {
      arr[i] = height
    }
  7. 使用requestAnimationFrame

    JAVASCRIPT
    // ✅ 与渲染帧同步
    function animate() {
      requestAnimationFrame(() => {
        // 修改样式
        updateStyle()
      })
    }

7. 什么是合成层(Composite Layer)?如何创建?

难度:⭐⭐⭐(高级)
标签:#渲染 #GPU #合成层

问题描述
解释浏览器合成层的概念,以及如何手动创建合成层优化性能。

参考答案要点

合成层概念

浏览器渲染时,会将页面分为多个图层(Layer),每个图层独立绘制,最后GPU合成。合成层的变化不会影响其他图层,避免重排和重绘。

创建合成层的条件

自动创建

  • 3D变换(transform: translate3d/rotate3d)
  • opacity动画
  • transform动画
  • will-change: transform/opacity
  • video/canvas/iframe元素
  • position: fixed
  • 重叠在合成层上的元素

手动创建

CSS
.gpu-layer {
  transform: translateZ(0);
  /* 或 */
  transform: translate3d(0, 0, 0);
  /* 或 */
  will-change: transform;
}

合成层优化

优点

  • 避免重排和重绘
  • GPU加速,适合动画
  • 减少主线程压力

缺点

  • 占用更多内存
  • 过多图层导致合成开销
  • 可能引发性能问题

最佳实践

  • 只为需要动画的元素创建合成层
  • 动画结束后移除will-change
  • 避免过多图层(建议不超过100个)

8. script标签的defer和async有什么区别?

难度:⭐⭐(中级)
标签:#浏览器 #JavaScript #性能

问题描述
详细解释script标签的defer和async属性的区别,以及使用场景。

参考答案要点

默认行为(无defer/async)

  • 遇到<script>立即下载并执行
  • 阻塞HTML解析
  • 阻塞DOM构建
  • 阻塞后续资源下载

async属性

HTML
<script async src="script.js"></script>
  • 下载:不阻塞HTML解析(并行下载)
  • 执行:下载完成后立即执行,阻塞HTML解析
  • 顺序:不保证执行顺序(谁先下载完谁先执行)
  • 适用:独立脚本,不依赖其他脚本(如统计代码、广告)

defer属性

HTML
<script defer src="script.js"></script>
  • 下载:不阻塞HTML解析(并行下载)
  • 执行:HTML解析完成后、DOMContentLoaded事件前执行
  • 顺序:保证执行顺序(按出现顺序)
  • 适用:依赖DOM或其他脚本的脚本

对比总结

特性无属性asyncdefer
下载阻塞
执行阻塞是(下载完)
执行时机立即下载完DOM构建后
执行顺序按顺序不保证按顺序
DOM就绪

最佳实践

HTML
<!-- 首屏关键JS -->
<script src="critical.js"></script>

<!-- 非关键JS -->
<script defer src="app.js"></script>

<!-- 独立第三方脚本 -->
<script async src="analytics.js"></script>

9. 什么是Critical Rendering Path(关键渲染路径)?如何优化?

难度:⭐⭐⭐(高级)
标签:#性能优化 #渲染

问题描述
解释关键渲染路径的概念,以及如何优化以提升首屏加载速度。

参考答案要点

关键渲染路径定义

浏览器将HTML、CSS、JavaScript转换为屏幕上像素所经历的步骤序列。优化目标是最小化首屏渲染时间

关键资源

  • 关键资源:阻塞首次渲染的资源
  • 关键路径长度:关键资源数量和大小
  • 关键字节数:关键资源的总大小

优化策略

  1. 最小化关键资源

    • 内联关键CSS(<style>标签)
    • 延迟加载非关键CSS(media="print" + onload
    • 异步加载非关键JS(defer/async)
  2. 优化关键资源大小

    • 压缩(Gzip/Brotli)
    • 代码分割(Code Splitting)
    • Tree Shaking
  3. 优化加载顺序

    HTML
    <!-- 1. 关键CSS内联 -->
    <style>
      /* 首屏CSS */
    </style>
    
    <!-- 2. 预加载关键资源 -->
    <link rel="preload" href="critical.js" as="script" />
    
    <!-- 3. 异步加载非关键CSS -->
    <link
      rel="preload"
      href="non-critical.css"
      as="style"
      onload="this.rel='stylesheet'"
    />
    
    <!-- 4. defer加载JS -->
    <script defer src="app.js"></script>
  4. 使用Preload/Prefetch

    HTML
    <!-- 预加载当前页面关键资源 -->
    <link rel="preload" href="font.woff2" as="font" crossorigin />
    
    <!-- 预获取下一页面资源 -->
    <link rel="prefetch" href="next-page.js" />
    
    <!-- DNS预解析 -->
    <link rel="dns-prefetch" href="//api.example.com" />
    
    <!-- 预连接 -->
    <link rel="preconnect" href="https://cdn.example.com" />

三、性能优化实战

10. 如何统计全站每个静态资源的加载耗时?

难度:⭐⭐⭐(高级)
标签:#性能监控 #Performance API

问题描述
设计一个方案,统计网站所有静态资源(JS、CSS、图片等)的加载耗时。

参考答案要点

方案一:Performance API

JAVASCRIPT
// 使用PerformanceObserver监听资源加载
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === "resource") {
      console.log({
        name: entry.name, // 资源URL
        type: entry.initiatorType, // 资源类型(script/link/img)
        duration: entry.duration, // 加载耗时
        startTime: entry.startTime,
        // 详细时间分解
        dns: entry.domainLookupEnd - entry.domainLookupStart,
        tcp: entry.connectEnd - entry.connectStart,
        request: entry.responseStart - entry.requestStart,
        response: entry.responseEnd - entry.responseStart,
      })
    }
  }
})

observer.observe({ entryTypes: ["resource"] })

方案二:Performance Timeline

JAVASCRIPT
// 获取所有资源加载数据
window.addEventListener("load", () => {
  const resources = performance.getEntriesByType("resource")

  const data = resources.map((r) => ({
    name: r.name,
    type: r.initiatorType,
    duration: r.duration,
    size: r.transferSize, // 传输大小
    decodedSize: r.decodedBodySize,
  }))

  // 上报到监控系统
  reportToServer(data)
})

方案三:结合错误监控

JAVASCRIPT
// 监控资源加载失败
window.addEventListener(
  "error",
  (e) => {
    if (e.target.tagName) {
      console.error("资源加载失败:", {
        url: e.target.src || e.target.href,
        type: e.target.tagName,
      })
    }
  },
  true
)

优化建议

  1. 缓存策略:分析缓存命中率,优化Cache-Control
  2. CDN优化:分析不同CDN节点性能
  3. 资源合并:减少HTTP请求数
  4. 预加载:识别关键资源进行preload
  5. 懒加载:非首屏资源懒加载

11. 如何实现前端性能监控SDK?

难度:⭐⭐⭐(高级)
标签:#性能监控 #SDK设计

问题描述
设计一个前端性能监控SDK,采集Web Vitals等关键性能指标。

参考答案要点

核心指标

JAVASCRIPT
class PerformanceMonitor {
  constructor() {
    this.metrics = {}
    this.init()
  }

  init() {
    this.observeNavigation()
    this.observePaint()
    this.observeLCP()
    this.observeCLS()
    this.observeFID()
    this.observeResources()
  }

  // 导航计时
  observeNavigation() {
    window.addEventListener("load", () => {
      setTimeout(() => {
        const nav = performance.getEntriesByType("navigation")[0]
        this.metrics.navigation = {
          dns: nav.domainLookupEnd - nav.domainLookupStart,
          tcp: nav.connectEnd - nav.connectStart,
          ttfb: nav.responseStart - nav.requestStart,
          domReady: nav.domContentLoadedEventEnd - nav.startTime,
          loadComplete: nav.loadEventEnd - nav.startTime,
        }
      }, 0)
    })
  }

  // 首次绘制
  observePaint() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name === "first-contentful-paint") {
          this.metrics.fcp = entry.startTime
        }
        if (entry.name === "first-paint") {
          this.metrics.fp = entry.startTime
        }
      }
    })
    observer.observe({ entryTypes: ["paint"] })
  }

  // 最大内容绘制
  observeLCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      this.metrics.lcp = lastEntry.startTime
    })
    observer.observe({ entryTypes: ["largest-contentful-paint"] })
  }

  // 累积布局偏移
  observeCLS() {
    let clsValue = 0
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value
        }
      }
      this.metrics.cls = clsValue
    })
    observer.observe({ entryTypes: ["layout-shift"] })
  }

  // 首次输入延迟
  observeFID() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.metrics.fid = entry.processingStart - entry.startTime
      }
    })
    observer.observe({ entryTypes: ["first-input"] })
  }

  // 上报数据
  report() {
    const data = {
      url: location.href,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      metrics: this.metrics,
    }

    // 使用sendBeacon确保数据发送
    navigator.sendBeacon("/api/performance", JSON.stringify(data))
  }
}

// 使用
const monitor = new PerformanceMonitor()
window.addEventListener("beforeunload", () => monitor.report())

关键指标说明

指标全称说明目标值
FPFirst Paint首次绘制< 1.8s
FCPFirst Contentful Paint首次内容绘制< 1.8s
LCPLargest Contentful Paint最大内容绘制< 2.5s
FIDFirst Input Delay首次输入延迟< 100ms
CLSCumulative Layout Shift累积布局偏移< 0.1
TTFBTime to First Byte首字节时间< 600ms

12. 如何实现图片懒加载?

难度:⭐⭐(中级)
标签:#性能优化 #懒加载

问题描述
实现一个图片懒加载方案,当图片进入视口时才加载。

参考答案要点

方案一:Intersection Observer API(推荐)

JAVASCRIPT
function lazyLoadImages() {
  const imageObserver = new IntersectionObserver(
    (entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target
          img.src = img.dataset.src // 从data-src加载真实图片
          img.classList.remove("lazy")
          observer.unobserve(img)
        }
      })
    },
    {
      rootMargin: "50px 0px", // 提前50px开始加载
      threshold: 0.01,
    }
  )

  document.querySelectorAll("img.lazy").forEach((img) => {
    imageObserver.observe(img)
  })
}

// HTML
// <img class="lazy" data-src="real-image.jpg" src="placeholder.jpg">

方案二:原生loading属性(最简单)

HTML
<img src="image.jpg" loading="lazy" alt="描述" />
  • loading="lazy":延迟加载
  • loading="eager":立即加载(默认)

方案三:scroll事件(兼容性最好)

JAVASCRIPT
function lazyLoad() {
  const images = document.querySelectorAll("img[data-src]")

  images.forEach((img) => {
    const rect = img.getBoundingClientRect()
    const isInViewport = rect.top < window.innerHeight && rect.bottom > 0

    if (isInViewport) {
      img.src = img.dataset.src
      img.removeAttribute("data-src")
    }
  })
}

// 节流优化
const throttledLazyLoad = throttle(lazyLoad, 200)
window.addEventListener("scroll", throttledLazyLoad)
window.addEventListener("resize", throttledLazyLoad)

优化技巧

  1. 占位图:使用低质量占位图或骨架屏
  2. 预加载可视区外:rootMargin提前加载
  3. 响应式图片:使用srcset
    HTML
    <img data-srcset="small.jpg 300w, large.jpg 800w" loading="lazy" />

13. 如何实现无限滚动加载?

难度:⭐⭐(中级)
标签:#交互 #性能

问题描述
实现一个无限滚动加载方案,当滚动到底部时自动加载更多内容。

参考答案要点

JAVASCRIPT
class InfiniteScroll {
  constructor(options) {
    this.container = options.container
    this.loadMore = options.loadMore
    this.threshold = options.threshold || 100
    this.isLoading = false
    this.hasMore = true

    this.init()
  }

  init() {
    // 使用Intersection Observer监听底部元素
    const sentinel = document.createElement("div")
    sentinel.className = "scroll-sentinel"
    this.container.appendChild(sentinel)

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !this.isLoading && this.hasMore) {
            this.load()
          }
        })
      },
      {
        rootMargin: `${this.threshold}px`,
      }
    )

    observer.observe(sentinel)
  }

  async load() {
    this.isLoading = true
    this.showLoading()

    try {
      const result = await this.loadMore()
      if (!result.hasMore) {
        this.hasMore = false
        this.showNoMore()
      }
    } catch (error) {
      this.showError()
    } finally {
      this.isLoading = false
      this.hideLoading()
    }
  }

  showLoading() {
    // 显示加载中提示
  }

  hideLoading() {
    // 隐藏加载中提示
  }

  showNoMore() {
    // 显示没有更多数据
  }

  showError() {
    // 显示加载失败,支持重试
  }
}

// 使用
const infiniteScroll = new InfiniteScroll({
  container: document.querySelector(".list-container"),
  threshold: 200,
  async loadMore() {
    const data = await fetch(`/api/list?page=${++this.page}`).then((r) =>
      r.json()
    )
    renderItems(data.items)
    return { hasMore: data.hasMore }
  },
})

优化建议

  1. 防抖/节流:避免频繁触发加载
  2. 加载状态:防止重复请求
  3. 错误处理:失败时支持重试
  4. 结束状态:无更多数据时提示
  5. 虚拟列表:大量数据时使用虚拟滚动

14. 如何防止前端重复请求?

难度:⭐⭐(中级)
标签:#网络 #性能

问题描述
设计一个方案,防止用户快速点击按钮导致的重复请求。

参考答案要点

方案一:请求锁(推荐)

JAVASCRIPT
class RequestLock {
  constructor() {
    this.pendingRequests = new Map()
  }

  async request(key, requestFn) {
    // 如果有进行中的请求,返回该请求的Promise
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key)
    }

    // 创建新请求
    const promise = requestFn().finally(() => {
      this.pendingRequests.delete(key)
    })

    this.pendingRequests.set(key, promise)
    return promise
  }
}

// 使用
const requestLock = new RequestLock()

async function submitForm(data) {
  return requestLock.request("submitForm", () => {
    return fetch("/api/submit", {
      method: "POST",
      body: JSON.stringify(data),
    })
  })
}

方案二:AbortController取消重复请求

JAVASCRIPT
class RequestManager {
  constructor() {
    this.controllers = new Map()
  }

  async request(key, requestFn) {
    // 取消之前的请求
    if (this.controllers.has(key)) {
      this.controllers.get(key).abort()
    }

    const controller = new AbortController()
    this.controllers.set(key, controller)

    try {
      const result = await requestFn(controller.signal)
      return result
    } finally {
      this.controllers.delete(key)
    }
  }
}

// 使用
const manager = new RequestManager()

function search(keyword) {
  return manager.request("search", (signal) => {
    return fetch(`/api/search?q=${keyword}`, { signal })
  })
}

方案三:防抖(搜索场景)

JAVASCRIPT
import { debounce } from "lodash"

const debouncedSearch = debounce((keyword) => {
  fetch(`/api/search?q=${keyword}`)
    .then((r) => r.json())
    .then((data) => renderResults(data))
}, 300)

// 输入时调用
input.addEventListener("input", (e) => {
  debouncedSearch(e.target.value)
})

方案四:按钮状态控制

JAVASCRIPT
async function handleSubmit() {
  const btn = document.querySelector(".submit-btn")

  if (btn.disabled) return

  btn.disabled = true
  btn.textContent = "提交中..."

  try {
    await submitForm()
    showSuccess()
  } catch (error) {
    showError()
  } finally {
    btn.disabled = false
    btn.textContent = "提交"
  }
}

四、业务场景实现

15. 如何实现一个购物车功能?

难度:⭐⭐⭐(高级)
标签:#业务场景 #状态管理

问题描述
设计一个购物车系统,包括添加商品、修改数量、计算价格、持久化存储等功能。

参考答案要点

数据结构设计

TYPESCRIPT
interface CartItem {
  skuId: string;           // SKU唯一标识
  productId: string;       // 商品ID
  name: string;            // 商品名称
  price: number;           // 单价(分)
  quantity: number;        // 数量
  selected: boolean;       // 是否选中
  image: string;           // 商品图片
  stock: number;           // 库存
  attributes: Record<string, string>; // 规格属性
}

interface CartState {
  items: CartItem[];
  isLoading: boolean;
}

核心功能实现

JAVASCRIPT
class ShoppingCart {
  constructor() {
    this.items = this.loadFromStorage() || []
  }

  // 添加商品
  addItem(product) {
    const existingItem = this.items.find((item) => item.skuId === product.skuId)

    if (existingItem) {
      // 已存在,增加数量
      existingItem.quantity += product.quantity
      // 检查库存
      if (existingItem.quantity > product.stock) {
        existingItem.quantity = product.stock
        throw new Error("超出库存限制")
      }
    } else {
      // 新商品
      this.items.push({
        ...product,
        selected: true,
      })
    }

    this.saveToStorage()
    this.syncToServer()
  }

  // 更新数量
  updateQuantity(skuId, quantity) {
    const item = this.items.find((item) => item.skuId === skuId)
    if (!item) return

    if (quantity <= 0) {
      this.removeItem(skuId)
    } else {
      item.quantity = Math.min(quantity, item.stock)
    }

    this.saveToStorage()
    this.syncToServer()
  }

  // 删除商品
  removeItem(skuId) {
    this.items = this.items.filter((item) => item.skuId !== skuId)
    this.saveToStorage()
    this.syncToServer()
  }

  // 切换选中状态
  toggleSelect(skuId) {
    const item = this.items.find((item) => item.skuId === skuId)
    if (item) {
      item.selected = !item.selected
      this.saveToStorage()
    }
  }

  // 全选/取消全选
  toggleSelectAll(selected) {
    this.items.forEach((item) => {
      item.selected = selected
    })
    this.saveToStorage()
  }

  // 计算总价
  getTotalPrice() {
    return this.items
      .filter((item) => item.selected)
      .reduce((total, item) => {
        return total + item.price * item.quantity
      }, 0)
  }

  // 计算总数量
  getTotalCount() {
    return this.items
      .filter((item) => item.selected)
      .reduce((count, item) => count + item.quantity, 0)
  }

  // 本地存储
  saveToStorage() {
    localStorage.setItem("cart", JSON.stringify(this.items))
  }

  loadFromStorage() {
    const data = localStorage.getItem("cart")
    return data ? JSON.parse(data) : null
  }

  // 同步到服务端(登录状态)
  async syncToServer() {
    if (this.isLoggedIn()) {
      await fetch("/api/cart/sync", {
        method: "POST",
        body: JSON.stringify(this.items),
      })
    }
  }

  // 合并本地和云端购物车(登录时)
  async mergeCart(serverCart) {
    const localCart = this.items

    // 合并策略:以本地为准,数量累加
    const merged = [...localCart]

    serverCart.forEach((serverItem) => {
      const localItem = merged.find((item) => item.skuId === serverItem.skuId)
      if (localItem) {
        localItem.quantity += serverItem.quantity
      } else {
        merged.push(serverItem)
      }
    })

    this.items = merged
    this.saveToStorage()
  }
}

未登录 vs 登录状态

场景存储方式同步策略
未登录localStorage登录时合并到服务端
已登录localStorage + 服务端实时同步,多端同步

16. 如何实现用户登录流程?

难度:⭐⭐(中级)
标签:#业务场景 #安全

问题描述
设计一个完整的用户登录流程,包括token管理、登录状态保持、单点登录等。

参考答案要点

登录流程

Plain Text
用户输入账号密码
       ↓
前端验证格式
       ↓
发送登录请求
       ↓
服务端验证 → 生成Token
       ↓
存储Token(Cookie/LocalStorage)
       ↓
获取用户信息
       ↓
跳转首页

Token管理

JAVASCRIPT
class AuthManager {
  constructor() {
    this.token = null
    this.refreshToken = null
    this.user = null
  }

  // 登录
  async login(credentials) {
    const response = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify(credentials),
    })

    const { token, refreshToken, user } = await response.json()

    this.setToken(token)
    this.setRefreshToken(refreshToken)
    this.setUser(user)

    // 启动自动刷新
    this.startRefreshTimer()

    return user
  }

  // 设置Token
  setToken(token) {
    this.token = token
    // 方案1:localStorage(易受XSS攻击)
    localStorage.setItem("token", token)

    // 方案2:Cookie(HttpOnly,防XSS)
    // document.cookie = `token=${token}; HttpOnly; Secure; SameSite=Strict`;
  }

  // 获取Token
  getToken() {
    return this.token || localStorage.getItem("token")
  }

  // 刷新Token
  async refreshToken() {
    try {
      const response = await fetch("/api/refresh", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${this.refreshToken}`,
        },
      })

      const { token } = await response.json()
      this.setToken(token)
      return token
    } catch (error) {
      // 刷新失败,退出登录
      this.logout()
      throw error
    }
  }

  // 自动刷新
  startRefreshTimer() {
    // Token过期前5分钟刷新
    const refreshTime = (getTokenExpiry(this.token) - 300) * 1000

    setTimeout(() => {
      this.refreshToken()
    }, refreshTime)
  }

  // 退出登录
  logout() {
    this.token = null
    this.refreshToken = null
    this.user = null
    localStorage.removeItem("token")
    localStorage.removeItem("refreshToken")

    // 通知服务端(可选)
    fetch("/api/logout", { method: "POST" })
  }

  // 检查登录状态
  isAuthenticated() {
    return !!this.getToken() && !isTokenExpired(this.getToken())
  }
}

请求拦截器(自动携带Token)

JAVASCRIPT
// axios拦截器
axios.interceptors.request.use((config) => {
  const token = authManager.getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器(Token过期自动刷新)
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config

    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true

      try {
        const newToken = await authManager.refreshToken()
        originalRequest.headers.Authorization = `Bearer ${newToken}`
        return axios(originalRequest)
      } catch (refreshError) {
        // 刷新失败,跳转登录
        window.location.href = "/login"
        return Promise.reject(refreshError)
      }
    }

    return Promise.reject(error)
  }
)

17. 如何实现前端路由?

难度:⭐⭐⭐(高级)
标签:#路由 #原理

问题描述
实现一个简单的前端路由系统,支持hash模式和history模式。

参考答案要点

Hash模式实现

JAVASCRIPT
class HashRouter {
  constructor() {
    this.routes = new Map()
    this.currentPath = ""

    // 监听hash变化
    window.addEventListener("hashchange", () => {
      this.render()
    })

    // 初始化
    window.addEventListener("load", () => {
      this.render()
    })
  }

  // 注册路由
  route(path, handler) {
    this.routes.set(path, handler)
  }

  // 导航
  navigate(path) {
    window.location.hash = path
  }

  // 渲染
  render() {
    const hash = window.location.hash.slice(1) || "/"
    this.currentPath = hash

    const handler = this.routes.get(hash)
    if (handler) {
      handler()
    } else {
      // 404处理
      const notFound = this.routes.get("/404")
      if (notFound) notFound()
    }
  }

  // 前进/后退
  back() {
    window.history.back()
  }

  forward() {
    window.history.forward()
  }
}

// 使用
const router = new HashRouter()

router.route("/", () => {
  document.getElementById("app").innerHTML = "<h1>首页</h1>"
})

router.route("/about", () => {
  document.getElementById("app").innerHTML = "<h1>关于</h1>"
})

// 导航
router.navigate("/about")

History模式实现

JAVASCRIPT
class HistoryRouter {
  constructor() {
    this.routes = new Map()

    // 监听popstate事件(前进/后退)
    window.addEventListener("popstate", () => {
      this.render()
    })
  }

  // 注册路由
  route(path, handler) {
    this.routes.set(path, handler)
  }

  // 导航(不刷新页面)
  navigate(path) {
    history.pushState({}, "", path)
    this.render()
  }

  // 替换当前历史记录
  replace(path) {
    history.replaceState({}, "", path)
    this.render()
  }

  // 渲染
  render() {
    const path = window.location.pathname

    const handler = this.routes.get(path)
    if (handler) {
      handler()
    } else {
      const notFound = this.routes.get("/404")
      if (notFound) notFound()
    }
  }

  // 前进/后退
  back() {
    window.history.back()
  }

  forward() {
    window.history.forward()
  }

  go(n) {
    window.history.go(n)
  }
}

// 使用
const router = new HistoryRouter()

router.route("/", () => {
  document.getElementById("app").innerHTML = "<h1>首页</h1>"
})

router.route("/user/:id", (params) => {
  document.getElementById("app").innerHTML = `<h1>用户 ${params.id}</h1>`
})

// 需要服务端配合,所有路由指向index.html

对比

特性Hash模式History模式
URL格式/#/path/path
服务端支持不需要需要(所有路由指向index.html)
兼容性IE8+IE10+
SEO较差较好

18. 如何实现一个虚拟列表?

难度:⭐⭐⭐(高级)
标签:#性能优化 #大数据

问题描述
实现一个虚拟列表,只渲染可视区域内的元素,优化大量数据的渲染性能。

参考答案要点

VUE
<template>
  <div class="virtual-list" ref="container" @scroll="handleScroll">
    <!-- 撑开滚动区域 -->
    <div class="list-phantom" :style="{ height: totalHeight + 'px' }"></div>

    <!-- 可视区域 -->
    <div
      class="list-content"
      :style="{ transform: `translateY(${offsetY}px)` }"
    >
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    items: Array, // 所有数据
    itemHeight: Number, // 每项高度
    buffer: {
      // 缓冲区数量
      type: Number,
      default: 5,
    },
  },

  data() {
    return {
      startIndex: 0, // 可视区起始索引
      endIndex: 0, // 可视区结束索引
      offsetY: 0, // 偏移量
      containerHeight: 0, // 容器高度
    }
  },

  computed: {
    // 可视区数据
    visibleItems() {
      return this.items.slice(this.startIndex, this.endIndex)
    },

    // 总高度
    totalHeight() {
      return this.items.length * this.itemHeight
    },
  },

  mounted() {
    this.containerHeight = this.$refs.container.clientHeight
    this.updateVisibleRange()
  },

  methods: {
    handleScroll() {
      const scrollTop = this.$refs.container.scrollTop
      this.updateVisibleRange(scrollTop)
    },

    updateVisibleRange(scrollTop = 0) {
      // 计算可视区起始索引
      this.startIndex = Math.floor(scrollTop / this.itemHeight)

      // 计算可视区能显示的数量
      const visibleCount = Math.ceil(this.containerHeight / this.itemHeight)

      // 加上缓冲区
      this.startIndex = Math.max(0, this.startIndex - this.buffer)
      this.endIndex = Math.min(
        this.items.length,
        this.startIndex + visibleCount + this.buffer * 2
      )

      // 计算偏移量(让内容在正确位置显示)
      this.offsetY = this.startIndex * this.itemHeight
    },
  },
}
</script>

<style scoped>
.virtual-list {
  position: relative;
  height: 400px;
  overflow-y: auto;
}

.list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.list-content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}

.list-item {
  box-sizing: border-box;
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
  padding: 0 16px;
}
</style>

优化版本(支持不定高度)

JAVASCRIPT
class DynamicVirtualList {
  constructor(options) {
    this.items = options.items
    this.container = options.container
    this.estimatedHeight = options.estimatedHeight || 50

    // 缓存每项的实际高度
    this.heightCache = new Map()

    // 计算累计高度
    this.cumulativeHeights = this.calculateCumulativeHeights()

    this.init()
  }

  // 二分查找起始索引
  findStartIndex(scrollTop) {
    let left = 0
    let right = this.cumulativeHeights.length - 1

    while (left < right) {
      const mid = Math.floor((left + right) / 2)
      if (this.cumulativeHeights[mid] < scrollTop) {
        left = mid + 1
      } else {
        right = mid
      }
    }

    return left
  }

  // 更新某项高度
  updateItemHeight(index, height) {
    this.heightCache.set(index, height)
    this.cumulativeHeights = this.calculateCumulativeHeights()
  }

  calculateCumulativeHeights() {
    const heights = []
    let sum = 0

    for (let i = 0; i < this.items.length; i++) {
      heights.push(sum)
      sum += this.heightCache.get(i) || this.estimatedHeight
    }

    return heights
  }
}

五、安全与监控

19. 如何防范XSS攻击?

难度:⭐⭐⭐(高级)
标签:#安全 #XSS

问题描述
详细描述XSS攻击的类型和防范措施。

参考答案要点

XSS类型

  1. 存储型XSS

    • 恶意脚本存储在服务端(数据库)
    • 所有访问该页面的用户都会执行
    • 危害最大
  2. 反射型XSS

    • 恶意脚本在URL参数中
    • 需要诱骗用户点击链接
    • 一次性攻击
  3. DOM型XSS

    • 通过JavaScript修改DOM时触发
    • 不经过服务端

防范措施

  1. 输入过滤

    JAVASCRIPT
    // 转义特殊字符
    function escapeHtml(str) {
      return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;")
    }
  2. 输出编码

    JAVASCRIPT
    // 根据输出位置选择编码方式
    // HTML内容
    element.textContent = userInput
    
    // HTML属性
    element.setAttribute("data-value", userInput)
    
    // JavaScript
    const script = document.createElement("script")
    script.textContent = JSON.stringify(userInput)
  3. CSP(内容安全策略)

    HTML
    <!-- 只允许加载同域资源 -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' 'unsafe-inline';"
    />
  4. HttpOnly Cookie

    JAVASCRIPT
    // 防止JavaScript读取Cookie
    document.cookie = "session=xxx; HttpOnly; Secure"
  5. 现代框架防护

    • React/Vue自动转义JSX/模板中的变量
    • 避免使用dangerouslySetInnerHTML/v-html

20. 如何防范CSRF攻击?

难度:⭐⭐⭐(高级)
标签:#安全 #CSRF

问题描述
详细描述CSRF攻击原理和防范措施。

参考答案要点

CSRF原理

  1. 用户登录A网站,获得Cookie
  2. 用户未退出,访问恶意网站B
  3. B网站自动发起请求到A网站
  4. 浏览器自动携带A网站的Cookie
  5. A网站认为是合法请求,执行操作

防范措施

  1. CSRF Token(最有效)

    JAVASCRIPT
    // 服务端生成Token,嵌入页面
    <meta name="csrf-token" content="随机Token">
    
    // 请求时携带
    fetch('/api/transfer', {
      method: 'POST',
      headers: {
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
      }
    });
  2. SameSite Cookie

    JAVASCRIPT
    // Strict: 完全禁止第三方Cookie
    // Lax: 允许部分情况(GET请求)
    // None: 允许,但必须配合Secure
    document.cookie = "session=xxx; SameSite=Strict; Secure"
  3. 验证Referer/Origin

    JAVASCRIPT
    // 服务端验证请求来源
    const referer = req.headers.referer
    if (!referer || !referer.startsWith("https://example.com")) {
      return res.status(403).send("Forbidden")
    }
  4. 双重Cookie验证

    JAVASCRIPT
    // Cookie中设置随机值,同时请求参数也携带该值
    // 服务端比较两个值是否一致

六、复习建议

重点题目清单

必会题目(⭐⭐⭐ 高频)

  1. 从输入URL到页面加载的完整过程
  2. TCP三次握手和四次挥手
  3. 重排和重绘的区别及优化
  4. 浏览器渲染原理
  5. 前端性能优化方案

重点掌握(⭐⭐ 常考)

  1. HTTPS握手过程
  2. DNS解析过程
  3. script标签的defer和async
  4. 事件循环机制
  5. 虚拟列表实现

加分项(⭐ 了解)

  1. 前端性能监控SDK
  2. 购物车/登录流程设计
  3. XSS/CSRF防范

复习方法

  1. 画图理解:网络流程、渲染流程、事件循环等建议画图
  2. 手写代码:防抖节流、Promise、深拷贝等必须能手写
  3. 项目结合:结合实际项目经验回答,更有说服力
  4. 性能数据:记住关键性能指标的目标值(如LCP < 2.5s)

面试技巧

  1. 结构化回答:分点作答,逻辑清晰
  2. 由浅入深:先讲基础,再讲优化和原理
  3. 结合实际:提到自己做过的性能优化案例
  4. 承认不足:遇到不会的问题,诚实说明,但表达学习意愿

最后更新于: 2026-02-27

目录