Skip to content

函数工具手写题

掌握防抖节流、this 绑定、函数柯里化等函数增强技巧

3. 防抖节流

难度: ⭐⭐ | 梯队: 第二梯队 | 标签: 闭包, 定时器

题目描述

  • 防抖 (debounce): 事件触发后延迟执行,期间再次触发则重新计时
  • 节流 (throttle): 事件触发后在一定时间内只执行一次

代码实现

javascript
/**
 * 防抖 - 基础版
 * @param {Function} fn - 要执行的函数
 * @param {number} delay - 延迟时间(ms)
 * @return {Function}
 */
function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    // 清除之前的定时器
    if (timer) clearTimeout(timer);

    // 设置新的定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

/**
 * 防抖 - 完整版
 * 支持:立即执行、取消、返回值
 */
function debounceAdvanced(fn, delay, immediate = false) {
  let timer = null;
  let result;

  const debounced = function (...args) {
    if (timer) clearTimeout(timer);

    if (immediate) {
      // 立即执行模式
      const callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, delay);

      if (callNow) {
        result = fn.apply(this, args);
      }
    } else {
      // 延迟执行模式
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, delay);
    }

    return result;
  };

  // 取消方法
  debounced.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  };

  return debounced;
}

/**
 * 节流 - 时间戳版(首次立即执行)
 */
function throttleTimestamp(fn, interval) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

/**
 * 节流 - 定时器版(延迟执行)
 */
function throttleTimer(fn, interval) {
  let timer = null;

  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, interval);
    }
  };
}

/**
 * 节流 - 完整版
 * 支持:首次执行、尾部执行、取消
 */
function throttle(fn, interval, options = {}) {
  const { leading = true, trailing = true } = options;
  let lastTime = 0;
  let timer = null;

  const throttled = function (...args) {
    const now = Date.now();

    // 首次不执行
    if (!leading && lastTime === 0) {
      lastTime = now;
    }

    const remaining = interval - (now - lastTime);

    if (remaining <= 0) {
      // 时间到了,立即执行
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastTime = now;
      fn.apply(this, args);
    } else if (!timer && trailing) {
      // 设置尾部执行
      timer = setTimeout(() => {
        lastTime = leading ? Date.now() : 0;
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };

  throttled.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    lastTime = 0;
  };

  return throttled;
}

示例调用

javascript
// ========== 防抖示例 ==========
// 基础用法
const debouncedLog = debounce((msg) => console.log(msg), 300);
debouncedLog('a');
debouncedLog('b');
debouncedLog('c'); // 只输出 'c'(300ms 后)

// 立即执行版
const debouncedImmediate = debounceAdvanced(
  (msg) => console.log(msg),
  300,
  true
);
debouncedImmediate('first'); // 立即输出
debouncedImmediate('second'); // 被忽略
// 300ms 后可以再次触发

// 取消防抖
const cancellable = debounceAdvanced((msg) => console.log(msg), 300);
cancellable('test');
cancellable.cancel(); // 取消,不会输出

// ========== 节流示例 ==========
// 基础用法
const throttledLog = throttle((msg) => console.log(msg), 1000);
throttledLog('a'); // 立即输出
throttledLog('b'); // 1秒内被忽略
throttledLog('c'); // 1秒内被忽略
// 1秒后再调用会立即输出

// 配置选项
const throttledNoLeading = throttle(
  (msg) => console.log(msg),
  1000,
  { leading: false, trailing: true }
);
throttledNoLeading('a'); // 不立即执行
// 1秒后输出 'a'

// 边界条件:delay 为 0
const debouncedZero = debounce((x) => x * 2, 0);
// 仍然会异步执行(进入事件循环)

// 边界条件:this 绑定
const obj = {
  value: 42,
  getValue: debounce(function () {
    console.log(this.value);
  }, 100),
};
obj.getValue(); // 100ms 后输出 42

复杂度分析

  • 时间: O(1)
  • 空间: O(1)

4. call / apply / bind

难度: ⭐⭐ | 梯队: 第二梯队 | 标签: this, 原型链

题目描述

实现 Function.prototype 上的 call、apply、bind 方法。

代码实现

javascript
/**
 * call 实现
 * 以指定 this 调用函数,参数逐个传入
 */
Function.prototype.myCall = function (context, ...args) {
  // 处理 null/undefined,默认指向全局对象
  context = context == null ? globalThis : Object(context);

  // 创建唯一属性名,避免覆盖原有属性
  const fn = Symbol('fn');
  context[fn] = this;

  // 调用并获取结果
  const result = context[fn](...args);

  // 删除临时属性
  delete context[fn];

  return result;
};

/**
 * apply 实现
 * 以指定 this 调用函数,参数以数组形式传入
 */
Function.prototype.myApply = function (context, args = []) {
  context = context == null ? globalThis : Object(context);

  const fn = Symbol('fn');
  context[fn] = this;

  const result = context[fn](...args);

  delete context[fn];

  return result;
};

/**
 * bind 实现
 * 返回一个绑定了 this 的新函数,支持 new 调用
 */
Function.prototype.myBind = function (context, ...args) {
  const self = this;

  const boundFunc = function (...newArgs) {
    // 判断是否是 new 调用
    // new 调用时 this 是 boundFunc 的实例
    const isNew = this instanceof boundFunc;
    return self.apply(isNew ? this : context, [...args, ...newArgs]);
  };

  // 维护原型关系,支持 new 调用
  if (this.prototype) {
    boundFunc.prototype = Object.create(this.prototype);
  }

  return boundFunc;
};

示例调用

javascript
// ========== call 示例 ==========
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: 'Alice' };

// 基础用法
console.log(greet.myCall(person, 'Hello', '!')); // 'Hello, Alice!'

// 边界条件:null/undefined
function getName() {
  return this === globalThis;
}
console.log(getName.myCall(null));      // true
console.log(getName.myCall(undefined)); // true

// 边界条件:原始类型会被包装
function getThis() {
  return this;
}
console.log(typeof getThis.myCall(42)); // 'object' (Number)

// 边界条件:无参数
function noArgs() {
  return this.value;
}
console.log(noArgs.myCall({ value: 100 })); // 100

// ========== apply 示例 ==========
console.log(greet.myApply(person, ['Hi', '?'])); // 'Hi, Alice?'

// 边界条件:空数组
console.log(noArgs.myApply({ value: 200 }, [])); // 200

// 边界条件:undefined 作为参数
console.log(noArgs.myApply({ value: 300 }, undefined)); // 300

// ========== bind 示例 ==========
const boundGreet = greet.myBind(person, 'Hey');
console.log(boundGreet('~')); // 'Hey, Alice~'

// 边界条件:多次 bind
const doubleBound = boundGreet.myBind({ name: 'Bob' });
console.log(doubleBound('!')); // 'Hey, Alice!' (第一次 bind 的 this 生效)

// 边界条件:new 调用
function Person(name) {
  this.name = name;
}
const BoundPerson = Person.myBind({ name: 'ignored' });
const instance = new BoundPerson('Charlie');
console.log(instance.name); // 'Charlie' (new 调用时忽略绑定的 this)
console.log(instance instanceof Person); // true

// 边界条件:无参数 bind
const bound = greet.myBind(person);
console.log(bound('Welcome', '!!')); // 'Welcome, Alice!!'

复杂度分析

  • 时间: O(1)
  • 空间: O(1)

6. 柯里化

难度: ⭐⭐ | 梯队: 第三梯队 | 标签: 闭包, 递归

题目描述

实现函数柯里化,将多参数函数转换为一系列单参数函数。

代码实现

javascript
/**
 * 通用柯里化函数
 * @param {Function} fn - 要柯里化的函数
 * @return {Function}
 */
function curry(fn) {
  return function curried(...args) {
    // 如果参数足够,直接调用
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    // 否则返回新函数,继续收集参数
    return function (...moreArgs) {
      return curried.apply(this, [...args, ...moreArgs]);
    };
  };
}

/**
 * 柯里化 - 支持占位符
 * 使用 _ 作为占位符
 */
function curryWithPlaceholder(fn, placeholder = curry.placeholder) {
  return function curried(...args) {
    // 检查是否有占位符
    const hasPlaceholder = args.some(arg => arg === placeholder);

    // 如果参数足够且没有占位符,直接调用
    if (args.length >= fn.length && !hasPlaceholder) {
      return fn.apply(this, args);
    }

    return function (...moreArgs) {
      // 用新参数替换占位符
      const newArgs = args.map(arg => 
        arg === placeholder && moreArgs.length ? moreArgs.shift() : arg
      );
      // 追加剩余的新参数
      return curried.apply(this, [...newArgs, ...moreArgs]);
    };
  };
}
curryWithPlaceholder.placeholder = Symbol('_');

/**
 * 链式调用的 add 函数
 * add(1)(2)(3) == 6
 */
function add(num) {
  let sum = num;

  function innerAdd(n) {
    sum += n;
    return innerAdd;
  }

  // 隐式类型转换
  innerAdd.toString = innerAdd.valueOf = () => sum;

  return innerAdd;
}

/**
 * 支持多参数的 add
 * add(1, 2)(3)(4, 5) == 15
 */
function addMulti(...args) {
  let sum = args.reduce((a, b) => a + b, 0);

  function innerAdd(...nextArgs) {
    sum += nextArgs.reduce((a, b) => a + b, 0);
    return innerAdd;
  }

  innerAdd.toString = innerAdd.valueOf = () => sum;

  return innerAdd;
}

示例调用

javascript
// ========== 通用柯里化 ==========
function add3(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add3);

// 基础用法
console.log(curriedAdd(1)(2)(3));       // 6
console.log(curriedAdd(1, 2)(3));       // 6
console.log(curriedAdd(1)(2, 3));       // 6
console.log(curriedAdd(1, 2, 3));       // 6

// 边界条件:无参数函数
const noArgsFn = curry(() => 42);
console.log(noArgsFn());                 // 42

// 边界条件:单参数函数
const singleArg = curry((x) => x * 2);
console.log(singleArg(5));               // 10

// 边界条件:保持 this 绑定
const obj = {
  value: 10,
  add: curry(function(a, b) {
    return this.value + a + b;
  })
};
console.log(obj.add(1)(2));              // 13

// ========== 占位符柯里化 ==========
const _ = curryWithPlaceholder.placeholder;
const curriedWithPlaceholder = curryWithPlaceholder(add3);

console.log(curriedWithPlaceholder(_, 2)(1)(3));  // 6 (a=1, b=2, c=3)
console.log(curriedWithPlaceholder(_, _, 3)(1)(2)); // 6

// ========== 链式 add ==========
console.log(add(1)(2)(3) == 6);          // true
console.log(+add(1)(2)(3)(4));           // 10

console.log(addMulti(1, 2)(3)(4, 5) == 15); // true
console.log(+addMulti(1)(2)(3)(4)(5));      // 15

// 边界条件:只调用一次
console.log(+add(5));                    // 5
console.log(+addMulti(1, 2, 3));         // 6

复杂度分析

  • 时间: O(n),n 为参数个数
  • 空间: O(n),闭包存储参数

前端面试知识库