logo
对话
写作
学术
绘画
社区

JavaScript防抖函数怎么写:基础实现与实用用法解答

此对话是使用 海鲸AI 创建的 • 发布时间:2026年11月14日 06:25

明天上海天气如何
已深度思考
17 tokens

User asks about Shanghai weather tomorrow. Need to call weather tool.

工具调用1
已调用工具s22__weather_search_local
展开
已调用工具s22__weather_search_local

上海明天白天有小雨,夜间转阴,全天温差不大,建议出门带伞。

📍 上海
☁️
19°阴
体感 21°湿度 95%风 东南风 1级 空气 优质 50
今天
11-13
☁️阴转中雨💧80%
16°
20°
明天
11-14
🌧️小雨转阴💧84%
15°
19°
后天
11-15
☁️阴💧10%
13°
19°
💡 明天白天有小雨、夜间转阴,风力不大但湿度很高(95%),出门带把伞,鞋子最好选防滑防水的

补充两句:明天紫外线最弱,不用特别防晒;不过空气湿度接近饱和,体感会偏闷黏,衣物不容易干,晚上洗完的衣服建议晾在通风处或直接烘干。后天转为阴天,降雨概率只剩约一成,适合安排户外行程。

帮我写个防抖函数
已深度思考
687 tokens

用户要求写一个防抖函数(debounce)。这是常见的 JavaScript 工具函数。我应该给出清晰的实现,包含基础版和带 immediate/leading 选项的进阶版,加上 TypeScript 类型和取消功能,并简要说明。

用户之前问天气,现在是技术问题,直接切换即可。

我来写一个完整但不过度冗长的版本。

基础版:

function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

进阶版:支持 immediate(首次立即执行)、cancel、flush、返回 Promise 或者保留返回值。

我写一个 TypeScript 版本比较完整,再给一个 JS 简化版。

注意 cancel 和 flush 的实现。

interface DebouncedFn<T extends (...args: any[]) => any> {
  (...args: Parameters<T>): void;
  cancel(): void;
  flush(): void;
  pending(): boolean;
}

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay = 300,
  options: { leading?: boolean; trailing?: boolean } = {}
): DebouncedFn<T> {
  const { leading = false, trailing = true } = options;
  let timer: ReturnType<typeof setTimeout> | null = null;
  let lastArgs: Parameters<T> | null = null;
  let lastThis: any = null;
  let result: ReturnType<T>;

  function invoke() {
    if (!lastArgs) return result;
    result = fn.apply(lastThis, lastArgs);
    lastArgs = lastThis = null;
    return result;
  }

  function debounced(this: any, ...args: Parameters<T>) {
    lastArgs = args;
    lastThis = this;

    const callNow = leading && !timer;

    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (trailing) invoke();
    }, delay);

    if (callNow) result = invoke();
    return result;
  }

  debounced.cancel = () => {
    if (timer) clearTimeout(timer);
    timer = null;
    lastArgs = lastThis = null;
  };

  debounced.flush = () => {
    if (timer) {
      clearTimeout(timer);
      timer = null;
      return invoke();
    }
    return result;
  };

  debounced.pending = () => timer !== null;

  return debounced as DebouncedFn<T>;
}

注意 invoke 里 leading 调用时会把 lastArgs 清空,但之后 trailing 的 setTimeout 仍会触发,此时 lastArgs 为空,invoke 返回 result,不会重复调用。不过有个细节:leading 调用后,如果用户在 delay 内又触发了一次,lastArgs 会被重新赋值,timer 重置,最终 trailing 会执行一次,这是正确的行为(标准 lodash 行为)。

不过还有个细节:leading 执行后,如果后续没有新调用,trailing 的 timer 到点后 invoke 发现 lastArgs 为 null,什么也不做——正确。

再给个 Promise 版本或者说明"等最后一次结果"的需求可以用 Promise 包装。

我用中文回答,配简单说明和用例。代码块用 js/ts。

还要提一下常见坑:

  • this 绑定
  • 参数透传
  • 返回值
  • 需要 cancel 清理(组件卸载)

保持简洁但实用。

基础版

function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

够用,但少了几个实际开发中常要的能力:this 之外的入参透传已经做了,还缺 立即执行、取消、返回值。

进阶版(推荐)

type Debounced<T extends (...args: any[]) => any> = {
  (...args: Parameters<T>): ReturnType<T> | undefined;
  cancel(): void;
  flush(): ReturnType<T> | undefined;
  pending(): boolean;
};

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay = 300,
  { leading = false, trailing = true } = {}
): Debounced<T> {
  let timer: ReturnType<typeof setTimeout> | null = null;
  let lastArgs: Parameters<T> | null = null;
  let lastThis: any = null;
  let result: ReturnType<T> | undefined;

  function invoke() {
    if (!lastArgs) return result;
    result = fn.apply(lastThis, lastArgs);
    lastArgs = lastThis = null;
    return result;
  }

  function debounced(this: any, ...args: Parameters<T>) {
    lastArgs = args;
    lastThis = this;

    const callNow = leading && !timer;

    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (trailing) invoke();
    }, delay);

    if (callNow) result = invoke();
    return result;
  }

  debounced.cancel = () => {
    if (timer) clearTimeout(timer);
    timer = null;
    lastArgs = lastThis = null;
  };

  debounced.flush = () => {
    if (timer) {
      clearTimeout(timer);
      timer = null;
      return invoke();
    }
    return result;
  };

  debounced.pending = () => timer !== null;

  return debounced as Debounced<T>;
}

用法

// 搜索框
const onSearch = debounce((kw) => fetch(`/api/search?q=${kw}`), 300);
input.addEventListener('input', (e) => onSearch(e.target.value));

// 按钮防连点:首次立即执行,防重复提交
const submit = debounce(doSubmit, 1000, { leading: true, trailing: false });

// 组件卸载前清理,避免定时器泄漏
onUnmounted(() => onSearch.cancel());

// 需要"立刻把这批输入提交掉"时
onSearch.flush();

几个容易踩的点

  1. this 和参数要透传:不用 apply(this, args) 的话,绑定在对象上会丢上下文。
  2. cancel 必须做:Vue/React 组件卸载后定时器还在,回调里访问已销毁的响应式数据会报错。
  3. 防抖 ≠ 节流:防抖是"停手后才执行",连续输入时一次都不触发;需要"匀速执行"(滚动、拖拽、resize)得用 throttle。
  4. 异步结果的坑:上面版本返回的是"最后一次同步调用"的值。如果 fn 是 async,debounced() 拿不到最终 Promise,需要额外做一个按 key 缓存 Promise 的版本,告诉我要不要写。
  5. flush 与 leading 同时用:leading 执行后 lastArgs 已清空,此时 flush() 返回缓存结果而不会重复调用,这个是符合预期的。
这个图片是什么
图片加载中
已深度思考
167 tokens

用户询问这张图片是什么。让我仔细观察图片内容。

这是一张展示壮观瀑布的风景照片。图片中可以看到:

  1. 一条巨大的瀑布从悬崖上倾泻而下
  2. 瀑布周围是绿色的植被覆盖的山坡/悬崖
  3. 前景有一些绿色植物和野花
  4. 天空呈现紫灰色调,可能是阴天或黄昏时分
  5. 瀑布的水流呈现出丝滑的效果,这通常是长曝光摄影的结果

这个瀑布看起来非常像冰岛的塞里雅兰瀑布(Seljalandsfoss),这是冰岛最著名的瀑布之一,位于冰岛南部。它的特点是:

  • 从悬崖边缘垂直落下
  • 周围有茂密的绿色植被
  • 游客可以走到瀑布后面
  • 高度约60米

不过我也应该提到,如果没有更多确认信息,我可以描述图片内容,但具体地点是基于视觉特征的推测。

这是一张壮观的瀑布风景照片,从画面特征来看,很可能是冰岛的塞里雅兰瀑布(Seljalandsfoss)。

图片内容描述:

元素 详情
主体 一条宽阔的瀑布从悬崖顶端垂直倾泻而下
拍摄手法 使用了长曝光技术,使水流呈现丝滑、如纱般的质感
周围环境 两侧是覆盖着浓密绿色苔藓和植被的悬崖峭壁
前景 有茂盛的绿草和一些野花/植物点缀
天空 紫灰色调的云层,营造出神秘而宁静的氛围
水雾 瀑布底部升起大量水雾

关于塞里雅兰瀑布:

📍 位置:冰岛南部,位于环岛公路(Route 1)附近
📏 高度:约 60 米
✨ 特色:这是冰岛最具标志性的瀑布之一,游客可以沿着小径走到瀑布背后观赏,体验非常独特
📸 摄影圣地:尤其是日落时分和阳光穿透水雾形成彩虹时,景色绝美

这张照片的构图非常出色——前景的植物增加了层次感,长曝光让瀑布如丝绸般柔美,整体色调偏冷,充满了北欧自然的静谧与力量感。