面试官:你能现场手写 debounce 吗?我:不仅能,还能解释 this!

307 阅读4分钟

当我们开发页面交互功能时,频繁触发的事件(如 scroll、resize、input)会影响性能,debounce(防抖)机制就成了解决这一问题的重要手段。而在实现 debounce 的过程中,很多人会忽略一个关键点:this 的处理

什么是 debounce(防抖)?

debounce(防抖)是一种 函数优化技术,其目的是:

某个函数被频繁调用时,只在最后一次调用后的固定时间内再执行一次。 多次调用,执行最后一次。

适用场景与代码示例

✅ 1. 搜索框联想(用户停止输入后再触发搜索)

📍 场景说明:

在搜索输入框中,用户每输入一个字符就发起一次 API 请求会非常消耗性能。防抖可让请求只在用户停止输入后一段时间后触发。

💡 示例代码:

<input id="search" placeholder="搜索..." />
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

function fetchSuggestions(event) {
  const query = event.target.value;
  console.log("发送搜索请求:", query);
}

const input = document.getElementById("search");
input.addEventListener("input", debounce(fetchSuggestions, 500));

✅ 2. resize / scroll 事件监听优化

📍 场景说明:

用户在拖动浏览器窗口或滚动页面时会不断触发事件。如果不加优化,函数会执行数十次每秒,影响性能。

💡 resize 示例:

window.addEventListener("resize", debounce(() => {
  console.log("窗口大小发生变化!");
}, 300));

💡 scroll 示例:

window.addEventListener("scroll", debounce(() => {
  console.log("滚动位置:", window.scrollY);
}, 200));

✅ 3. 表单输入验证

📍 场景说明:

在注册表单中,验证用户名是否已被占用,如果每输入一个字符就发送请求,会造成服务器压力。

💡 示例代码:

<input id="username" placeholder="请输入用户名" />
function checkUsername(e) {
  const username = e.target.value;
  console.log("验证用户名是否可用:", username);
  // 这里可以发送 Ajax 请求
}

document.getElementById("username").addEventListener("input", debounce(checkUsername, 600));

✅ 4. 按钮防止重复点击(防止多次提交)

📍 场景说明:

某些按钮触发提交操作时,如果用户连续点击,可能会造成表单重复提交、接口重复调用等问题。

💡 示例代码:

<button id="submitBtn">提交</button>
function handleSubmit() {
  console.log("表单已提交!");
  // 防止重复提交逻辑
}

document.getElementById("submitBtn").addEventListener(
  "click",
  debounce(handleSubmit, 1000)
);

🔍 用户点击按钮后 1 秒内再次点击不会触发 handleSubmit

为什么要使用 fn.apply(this, args)

🤔 常见误解:

许多初学者在实现 debounce 时写成:

setTimeout(() => {
  fn(); // ❌ 这样写会导致 this 丢失
}, delay);

这会导致:

  • 在对象方法中使用时,this 不再指向对象本身,而是变成了 undefined 或 window
  • 导致方法内部访问 this.xxx 出现错误

✅ 正确做法:

使用 fn.apply(this, args)

  • this:指向事件绑定对象(如 DOM、组件等)
  • args:保留调用时传入的参数

实际示例:

const btn = {
  text: "点击按钮",
  handleClick() {
    console.log(this.text);
  }
};

document.querySelector("#btn").addEventListener(
  "click",
  debounce(btn.handleClick, 500)
); 

如果 debounce 实现中没处理 this,这里会输出 undefined 或报错。


🎯 apply 是做什么的?

fn.apply(this, args);
  • apply 调用函数,并指定 this 的值和参数数组。
  • 相当于 fn(...args),但能明确设置调用时的上下文对象。

📦 五、进阶 debounce(支持立即执行 + 保留 this)

function debounce(fn, delay, immediate = false) {
  let timer = null;

  return function (...args) {
    const context = this;

    if (timer) clearTimeout(timer);

    if (immediate && !timer) {
      fn.apply(context, args);
    }

    timer = setTimeout(() => {
      timer = null;
      if (!immediate) {
        fn.apply(context, args);
      }
    }, delay);
  };
}

🛠 六、实战:搜索框 + this 绑定

<input id="search" placeholder="请输入关键词" />
const searchBox = {
  keyword: "",
  handleInput(e) {
    this.keyword = e.target.value;
    console.log("搜索关键词:", this.keyword);
  }
};

const inputEl = document.getElementById("search");

// 用 bind 保证 this 指向 searchBox
inputEl.addEventListener("input", debounce(searchBox.handleInput.bind(searchBox), 500));

总结

  • debounce 是高频事件优化利器;
  • 实现中一定要考虑 this 上下文问题
  • 建议使用 fn.apply(this, args) 保留正确上下文;
  • 可扩展立即执行、取消功能;
  • throttle 一起掌握前端性能优化两大利器。

🧾 写在最后

debounce(防抖)作为前端性能优化的重要技巧,能够有效减少高频率事件带来的性能消耗,提升用户体验。在搜索建议、窗口监听、表单验证、按钮防止重复点击等实际开发中都有着广泛的应用价值。

在实现 debounce 的过程中,别忘了 this 的正确绑定 —— 这不仅考验对 JS 函数执行上下文的理解,也是编写健壮工具函数的关键。

希望这篇文章能帮助你全面掌握 debounce 的原理、使用场景和实现方式,让你在开发中更加游刃有余。如果你觉得有收获,欢迎点赞 👍、收藏 ⭐、转发支持,让更多人一起进阶前端!