阅读视图

发现新文章,点击刷新页面。

防抖(Debounce)与节流(Throttle)

一、核心概念对比

维度 防抖(Debounce) 节流(Throttle)
定义 事件触发后,等待固定时间再执行回调;若期间再次触发,则重新计时。 事件触发后,固定时间内只执行一次回调,后续触发被忽略。
目标 确保高频事件(如输入)的最终结果只触发一次。 确保高频事件(如滚动)按固定频率执行。
执行时机 最后一次触发后的等待时间结束时执行。 按固定时间间隔执行(如每秒一次)。
类比 电梯关门:有人进出时重新计时关门。 水龙头滴水:无论拧多快,水滴按固定频率落下。

二、代码实现

1. 防抖(Debounce)

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer); // 清除之前的计时
    timer = setTimeout(() => {
      fn.apply(this, args); // 延迟执行
    }, delay);
  };
}

// 示例:输入框搜索建议
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function(e) {
  console.log('发起搜索请求:', e.target.value);
}, 500));

2. 节流(Throttle)

function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) { // 判断是否达到时间间隔
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 示例:滚动加载更多
window.addEventListener('scroll', throttle(function() {
  console.log('检查滚动位置,加载数据...');
}, 1000));

三、使用场景

场景 防抖(Debounce) 节流(Throttle)
输入框实时搜索 ✅ 用户停止输入后发起请求(如500ms无输入) ❌ 不适合(输入期间仍需响应)
窗口调整(resize) ✅ 调整结束后计算布局 ✅ 按固定频率更新布局(如每秒一次)
按钮防重复点击 ✅ 避免用户快速点击多次提交 ✅ 固定时间内只允许提交一次
滚动事件(scroll) ❌ 最后一次滚动后触发可能不符合预期 ✅ 滚动期间定期检查位置(如触底加载)
鼠标移动(mousemove) ❌ 需要连续响应时不适用 ✅ 限制高频更新(如拖拽元素时降低渲染频率)

四、高频面试问题

1. 如何手写防抖和节流?

  • 参考上述代码实现,注意闭包和apply绑定上下文。

2. 防抖和节流的本质区别是什么?

  • 防抖关注“最后一次触发后的结果”,节流关注“固定间隔内的执行频率”。

3. 如何选择防抖或节流?

  • 防抖:适合结果导向型场景(如搜索建议、提交按钮)。
  • 节流:适合过程控制型场景(如滚动加载、实时定位)。

4. 防抖的立即执行版本如何实现?

  • 添加参数控制是否立即执行第一次触发:

    function debounce(fn, delay, immediate) {
      let timer = null;
      return function(...args) {
        if (immediate && !timer) {
          fn.apply(this, args); // 立即执行
        }
        clearTimeout(timer);
        timer = setTimeout(() => {
          if (!immediate) fn.apply(this, args);
          timer = null;
        }, delay);
      };
    }
    

五、常见误区

  • 过度依赖第三方库:虽然Lodash的_.debounce_.throttle功能完善,但面试需掌握原生实现。
  • 忽视this和参数绑定:回调函数中需正确传递this和事件参数(如event)。
  • 时间间隔设置不合理:防抖的延迟过长会导致响应迟钝,节流的间隔过短会失去优化意义。

六、总结回答技巧

  • 一句话概括
    “防抖是延迟执行,确保连续触发只执行最后一次;节流是限频执行,确保连续触发按固定频率执行。”
  • 结合项目举例
    “在商品搜索页中,输入框用防抖减少请求次数;图片懒加载用节流控制滚动检查频率。”
  • 扩展思考
    “防抖和节流本质是闭包的应用,通过控制计时器和时间戳管理事件触发逻辑。”

Get与Post区别

1. 核心特性对比

维度 GET POST
语义 获取数据(幂等操作) 提交数据(非幂等操作)
参数位置 URL查询字符串(?key=value 请求体(Body)
数据长度限制 受URL长度限制(通常2KB~8KB,因浏览器/服务器而异) 无明确限制(适合传输大文件或表单数据)
安全性 参数明文暴露在URL中(易被缓存、日志记录) 参数在请求体中(相对安全,但仍需HTTPS加密)
缓存 可被浏览器缓存 默认不可缓存
历史记录 保留在浏览器历史记录中 不保留
书签 可收藏为书签(含参数) 不可收藏
编码类型 仅支持URL编码(application/x-www-form-urlencoded 支持多种编码(如multipart/form-dataJSON
幂等性 幂等(多次请求结果相同) 非幂等(多次请求可能产生副作用)

2. 使用场景

方法 典型场景 示例
GET - 获取资源(如搜索、分页) - 无副作用的操作 - 数据可公开或非敏感 GET /api/users?page=2 获取用户列表第二页数据
POST - 提交表单或文件 - 创建资源(如新增订单) - 敏感数据操作(如登录、支付) POST /api/login 提交用户名密码;POST /api/upload 上传文件

3. 技术细节

  • 幂等性
    GET的幂等性使其适合重复请求(如刷新页面),而POST重复提交可能导致重复创建资源(需后端防重处理)。

  • 性能
    GET请求可以被CDN或浏览器缓存,减少服务器压力;POST每次需服务端处理。

  • RESTful API规范

    • GET对应查询(Read)
    • POST对应创建(Create)
      (注:实际开发中可能不严格遵循,但语义清晰利于协作)

4. 安全性误区

  • GET“不安全”
    参数在URL中暴露,易被浏览器历史、服务器日志记录,但安全性最终取决于是否使用HTTPS。
    示例:GET传密码(错误)→ 即使HTTPS加密,URL中的密码仍可能被泄露。
  • POST“绝对安全”
    请求体数据仍可被抓包工具截获,敏感数据(如密码)需结合HTTPS和加密传输。

5. 面试回答技巧

  • 简明回答
    “GET用于获取数据,参数在URL中,可缓存且幂等;POST用于提交数据,参数在请求体中,适合敏感或大数据传输。”
  • 结合场景
    “在实现搜索功能时用GET,参数暴露便于分享链接;提交订单时用POST,避免重复下单和参数泄露。”
  • 深入扩展
    “从RESTful角度,GET对应Read,POST对应Create;实际开发中还可通过请求头区分语义(如X-HTTP-Method-Override)。”

Token、Cookie、Session的区别

1. 存储位置与机制

类型 存储位置 依赖关系 特点
Cookie 客户端(浏览器) 由服务器通过Set-Cookie响应头设置,自动随请求发送 大小受限(约4KB),可设置过期时间;存在安全风险(如CSRF攻击)。
Session 服务端(服务器内存/数据库) 依赖Cookie中的Session ID与服务端数据关联 用户状态存储在服务端,安全性更高;但服务器需维护Session数据,高并发时可能影响性能。
Token 客户端(如LocalStorage) 由服务端生成并返回,客户端手动在请求头(如Authorization)添加 无状态(服务端不存储),可自定义存储位置(如Header、URL参数);无跨域限制,适合分布式系统(如JWT)。

2. 安全性对比

类型 安全风险 防护措施
Cookie - CSRF攻击(跨站请求伪造) - XSS窃取Cookie内容(若未设置HttpOnly - 设置SameSite属性限制跨站发送 - 标记HttpOnly防止JS访问 - 使用Secure仅HTTPS传输
Session - Session劫持(若Session ID泄露) - 服务端存储压力大(可能被攻击者耗尽资源) - 定期清理过期Session - 结合IP/User-Agent验证 - 加密Session ID
Token - XSS窃取Token(若存储不当) - Token泄露后无法快速失效(需依赖黑名单或短有效期) - Token加密签名(如JWT的签名机制) - 存储于非持久化位置(如内存) - 设置合理过期时间

3. 使用场景

类型 适用场景 示例
Cookie - 简单的用户状态保持(如登录态) - 需要服务端自动管理的小型数据(如语言偏好) 电商网站的购物车ID存储
Session - 对安全性要求高的场景(如支付系统) - 需要服务端完全控制用户状态的系统 银行系统登录后的会话管理
Token - 前后端分离架构(如SPA应用) - 跨域/分布式系统(如微服务API鉴权) - 无状态身份验证(如移动端APP) JWT用于用户登录后访问API资源

4. 核心区别总结

维度 Cookie Session Token
存储位置 客户端 服务端 客户端
通信方式 自动随请求发送 依赖Cookie中的Session ID 手动添加到请求头/参数
跨域支持 受同源策略限制(需CORS) 同Cookie 无限制(可手动传递)
服务端压力 低(数据在客户端) 高(需存储所有Session数据) 低(无状态)
扩展性 受限(大小和安全性) 依赖服务端扩展 高(适合分布式系统)
典型应用 会话管理、跟踪用户行为 敏感操作的身份验证 API鉴权、移动端身份验证

5. 面试回答技巧

  • 简明回答
    “Cookie由服务端设置,存储在浏览器,自动随请求发送;Session数据在服务端,通过Session ID关联;Token(如JWT)由服务端生成,客户端存储并手动发送,无状态且适合分布式系统。”
  • 结合场景
    “在单页面应用(SPA)中,常用Token实现无状态鉴权,避免Cookie的跨域限制;而传统Web应用可能用Session管理敏感会话。”
  • 安全补充
    “Cookie需防范CSRF(如设置SameSite),Token需防范XSS(避免LocalStorage存储敏感数据)。”

JavaScript 事件委托详解与面试指南

一、事件委托核心概念

1. 基本定义

事件委托(Event Delegation)是一种利用事件冒泡机制,将子元素的事件处理委托给父元素统一管理的技术。

2. 实现原理

  • 事件冒泡:事件从触发元素向上传播到DOM树
  • 事件捕获:事件从window向下传递到目标元素(可选阶段)
  • 目标阶段:事件到达实际触发元素

3. 三阶段图示

捕获阶段: window -> document -> ... -> 父元素
目标阶段: 目标元素
冒泡阶段: 目标元素 -> ... -> document -> window

二、面试常见问题

基础问题

  1. 什么是事件委托?它的原理是什么?

    • 参考答案:事件委托是利用事件冒泡机制,在父元素上统一处理子元素事件的技术。原理是通过event.target识别实际触发事件的元素。
  2. 为什么要使用事件委托?

    • 参考答案:

      • 减少内存消耗(避免为每个子元素绑定事件)
      • 动态添加的元素自动拥有事件处理
      • 提高初始化性能(减少事件绑定次数)
  3. 如何实现基本的事件委托?

    document.getElementById('parent').addEventListener('click', function(event) {
      if (event.target.classList.contains('child')) {
        // 处理逻辑
      }
    });
    

进阶问题

  1. event.target 和 event.currentTarget 有什么区别?

    • event.target:实际触发事件的元素
    • event.currentTarget:当前正在处理事件的元素(委托的父元素)
  2. 如何处理动态生成的元素事件?

    // 使用closest方法处理动态内容
    document.addEventListener('click', function(event) {
      const btn = event.target.closest('.dynamic-btn');
      if (btn) {
        // 处理逻辑
      }
    });
    
  3. 哪些事件不支持冒泡?如何委托这些事件?

    • 不冒泡事件:focus、blur、load、unload等

    • 解决方案:

      • 使用支持冒泡的替代事件(focusin/focusout)
      • 手动在元素上绑定事件

深度问题

  1. 事件委托对性能有什么影响?如何优化?

    • 优点:减少事件监听器数量,降低内存占用

    • 缺点:深层嵌套时事件传播路径长

    • 优化:

      • 在最近的公共父元素上委托
      • 使用事件委托+节流处理高频事件
  2. 如何阻止事件委托?

    • event.stopPropagation():停止事件传播
    • event.stopImmediatePropagation():停止传播并阻止同元素其他处理程序
  3. 编写一个通用的事件委托函数

    function delegate(parent, eventType, selector, handler) {
      parent.addEventListener(eventType, function(event) {
        if (event.target.matches(selector)) {
          handler.call(event.target, event);
        }
      });
    }
    

三、实战编码题

题目1:实现表格行点击高亮

// 方案
document.querySelector('table').addEventListener('click', function(event) {
  const row = event.target.closest('tr');
  if (row) {
    // 移除其他行高亮
    document.querySelectorAll('tr.active').forEach(r => {
      r.classList.remove('active');
    });
    // 添加当前行高亮
    row.classList.add('active');
  }
});

题目2:动态列表删除功能

document.getElementById('list').addEventListener('click', function(event) {
  if (event.target.classList.contains('delete-btn')) {
    const item = event.target.closest('li');
    item.remove();
  }
});

四、面试加分点

  1. 对比直接绑定和委托的性能

    • 直接绑定:O(n)内存占用,初始化耗时长
    • 委托:O(1)内存占用,初始化快
  2. 事件委托的局限性

    • 不适合所有事件类型(如不冒泡的事件)
    • 深层嵌套时可能影响性能
    • 需要额外代码判断目标元素
  3. 现代替代方案

    • 使用框架内置功能(如React的合成事件)
    • 第三方事件委托库
  4. 内存泄漏防范

    • 及时移除不需要的委托监听器
    • 使用WeakMap存储处理函数

五、常见误区

  1. 在document上绑定所有委托

    • 问题:事件传播路径过长
    • 解决:在最近的公共父元素上绑定
  2. 过度使用event.stopPropagation()

    • 问题:破坏事件流,影响其他监听器
    • 解决:仅在必要时使用
  3. 忽略事件委托对动态内容的优势

    • 典型反模式:在动态添加元素后重新绑定事件

前端动画方案全面对比

问题:

之前用setTimeout实现了一个弹幕动画,动画跑着跑着画面越来越快,这是为什么?怎样解决?

问题原因分析

当使用 setTimeout 实现弹幕动画时出现越来越快的情况,通常由以下几个原因导致:

  1. 时间累积误差setTimeout 不能保证精确的时间间隔,每次执行可能有微小延迟,这些延迟会累积
  2. 回调执行时间:动画逻辑本身的执行时间会影响下一次调用的时机
  3. 事件循环机制setTimeout 受浏览器事件循环影响,优先级低于渲染等任务
  4. 未考虑帧同步:没有与屏幕刷新率同步,导致动画速度不稳定

怎样解决

  1. 对于现代浏览器,始终优先使用 requestAnimationFrame
  2. 如需支持旧浏览器,可使用以下polyfill:
window.requestAnimationFrame = window.requestAnimationFrame || 
  window.webkitRequestAnimationFrame || 
  window.mozRequestAnimationFrame ||
  function(callback) {
    window.setTimeout(callback, 1000 / 60);
  };
  1. 对于需要精确时间控制的动画,使用基于时间戳的计算方式
  2. 避免在动画中执行耗时操作,保持每帧执行时间在3-4ms以内

为什么requestAnimationFrame可以解决这个问题

1. 与浏览器刷新率硬同步

  • setTimeout 的问题
    即便设置为 16ms(模拟60fps),由于 JavaScript 单线程特性,setTimeout 回调可能被其他任务阻塞,导致实际执行间隔不稳定(可能变成 17ms、20ms 甚至更长)。这些延迟累积会让动画越来越快(因为代码中通常用固定步长位移,而非基于时间计算)。
  • requestAnimationFrame 的解决方案
    requestAnimationFrame 直接绑定到浏览器的渲染周期,在每次屏幕刷新前执行(通常严格保持 16.7ms/帧,60Hz 屏幕)。浏览器会智能合并 requestAnimationFrame 回调,确保动画与硬件刷新率同步,避免时间漂移。

2. 基于时间戳的自动补偿

  • requestAnimationFrame 的回调函数会自动接收一个 timestamp 参数(高精度时间戳),开发者可以用它计算真实的时间差,实现帧率无关的动画:

    let lastTime;
    function animate(timestamp) {
      if (!lastTime) lastTime = timestamp;
      const deltaTime = timestamp - lastTime; // 实际经过的时间
      lastTime = timestamp;
      
      // 根据 deltaTime 计算位移(避免固定步长导致的加速)
      element.style.left = (element.offsetLeft + speed * (deltaTime / 16.67)) + 'px';
      
      requestAnimationFrame(animate);
    }
    

    即使某帧延迟,deltaTime 也会按实际时间调整位移量,保持速度恒定。


3. 后台自动休眠

  • setTimeout 的问题
    即使页面隐藏,setTimeout 仍会继续执行,导致不必要的计算和电量消耗。
  • requestAnimationFrame 的解决方案
    当页面不可见(如切换标签页或最小化),浏览器会自动暂停 rAF 回调,恢复可见时继续执行。这既节省资源,又避免了不可见时的动画逻辑堆积(堆积的 setTimeout 回调会在页面恢复时集中执行,导致动画瞬间跳跃)。

4. 浏览器级优化

  • requestAnimationFrame 的优先级高于 setTimeout,浏览器会优先调度动画相关的渲染任务。
  • 对连续多个 requestAnimationFrame 调用,浏览器会合并处理,避免冗余计算(例如快速连续调用 requestAnimationFrame 时,浏览器可能只执行一次回调)。

对比代码示例

❌ setTimeout 的问题实现(会加速)
let pos = 0;
function move() {
  pos += 2; // 固定步长,时间漂移时必然加速
  element.style.left = pos + 'px';
  setTimeout(move, 16); // 无法保证严格 16ms
}
move();
✅ requestAnimationFrame 的正确实现
let pos = 0, lastTime;
function move(timestamp) {
  if (!lastTime) lastTime = timestamp;
  const deltaTime = timestamp - lastTime;
  lastTime = timestamp;
  
  pos += 2 * (deltaTime / 16.67); // 根据实际时间调整步长
  element.style.left = pos + 'px';
  
  requestAnimationFrame(move);
}
requestAnimationFrame(move);

总结表

问题根源 setTimeout 的表现 requestAnimationFrame 的解决方案
时间不同步 延迟累积导致动画加速 严格同步屏幕刷新率
后台资源浪费 隐藏页面仍执行动画 自动暂停回调
位移计算不精确 固定步长导致速度不稳定 基于时间戳动态计算位移
优先级低 可能被其他任务阻塞 浏览器优先调度

结论requestAnimationFrame 是专为动画设计的 API,从底层解决了 setTimeout 的时间同步问题,是现代 Web 动画的首选方案。

目前端动画实现方案概览

目前前端实现动画主要有以下几种方案:

  1. CSS 动画/过渡transitionanimation
  2. JavaScript 定时器setTimeoutsetInterval
  3. requestAnimationFrame:浏览器专为动画提供的API
  4. Web Animations API:较新的原生动画API
  5. 动画库:GSAP、Anime.js、Velocity.js等
  6. Canvas/SVG 动画:通过绘图API实现

requestAnimationFrame 详解

官方解释

window.requestAnimationFrame()  方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。

对回调函数的调用频率通常与显示器的刷新率相匹配。虽然 75hz、120hz 和 144hz 也被广泛使用,但是最常见的刷新率还是 60hz(每秒 60 个周期/帧)。为了提高性能和电池寿命,大多数浏览器都会暂停在后台选项卡或者隐藏的 <iframe> 中运行的 requestAnimationFrame()

原理

  1. 与浏览器刷新率同步:通常以 60fps (每16.7ms执行一次) 的速率执行,与屏幕刷新率保持一致
  2. 自动暂停:当页面不可见或最小化时,动画会自动暂停,节省 CPU/GPU 资源
  3. 浏览器优化:浏览器会合并多个 rAF 请求,进行统一处理

优势

对比 setTimeout/setInterval

  1. 更精确的时机控制:与屏幕刷新同步,避免丢帧
  2. 更高效:浏览器会优化执行,页面不可见时暂停
  3. 更省电:非活动页面自动停止动画

对比 CSS 动画

  1. 更灵活的控制:可以处理复杂逻辑和交互
  2. 更丰富的效果:可以实现 CSS 难以表达的动画
  3. 更好的性能监控:可以精确控制每一帧

各方案对比表格

对比维度 CSS 动画/过渡 JavaScript 定时器 requestAnimationFrame Web Animations API GSAP等动画库
实现复杂度 简单 中等 中等 中等 简单(API友好)
控制精度 低(关键帧之间) 最高
性能 高(浏览器优化) 低(可能丢帧) 最高(与渲染同步) 高(优化过)
资源消耗 中-高 中(库体积)
兼容性 好(需前缀) 极好 好(IE10+) 一般(较新浏览器) 好(兼容处理)
适用场景 简单UI动画 简单定时任务 复杂交互动画 复杂动画 专业复杂动画
能否暂停/继续 可以 可以 可以 可以 可以
时间控制 有限 精确 非常精确 精确 非常精确
GPU加速
代码示例 [见下方] [见下方] [见下方] [见下方] [略]
❌