Skip to content

js工具函数

一、关于函数

1、高阶函数

高阶函数有两个特点:

  • 将函数作为参数的函数(回调)
  • 返回值是一个函数的函数

只要满足以上两个特点中的一个的函数,就是高阶函数

1.1、before函数

高阶函数 before 函数是一个包装函数。也叫装饰器函数,在保证原有函数fn的逻辑不变的前提下,对fn进行包装扩展,以扩展更多功能

js
function fn(a, b, c) {
  console.log("todo...", a, b, c);
}

// 将before挂到Function原型上而不是fn原型上,避免了每次调用都要new 一下fn
Function.prototype.before = function (beforeFn) {
  return (...args) => {
    beforeFn(); // 先执行回调
    this(...args); // 再执行fn方法(因为这里运用了箭头函数,所以this指向before调用者fn)
  };
};

// 使用 -> 返回了一个函数
let newFn = fn.before(() => {
  console.log("fn before...");
});
// 只有调用了返回的函数,才会执行内部逻辑,
newFn("a", "b", "c");
/* 
输出:
    fn before...
    todo... a b c
*/

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,其实就是给原函数增加一层,不用管原函数内部实现

1.2、类型检测函数

类型检测的方法有4个typeofconstroctorinstanceofObject.prototype.toString.call;

  • typeof 不能区分对象类型
  • controctor 是判断构造函数
  • instanceof 是判断实例
  • Object.protortpe.toString.call 很准确

类型检测方法

js
function isType(value,typeing) {
    return Object.prototype.toString.call(value) === `[object ${typeing}]`
}
isType(123,'Number') // true

缺点: 不方便,不健壮。若是参数typeing传入错误就凉凉

改进 1、不让用户输入类型 2、创建一个utils,封装好各种方法,可以直接调用 3、函数分步传递参数,将函数拆分成功能更具体化的函数

js
function isType(value,typeing) {
    return Object.prototype.toString.call(value) === `[object ${typeing}]`
}

let utils = {};
['Number','String','Boolean','Array','Object'].forEach(typeing=>{
    utils[`is`+typeing] = function(val) {
        return isType(val,typeing)
    }
})

console.log(utils.isString('hello')); // true
console.log(utils.isString(123)); // false

1.3、柯里化函数

柯里化函数也是一个高阶函数,当传出的参数个数没有达到需要的参数总个数时,会将传入的参数进行缓存,直到参数个数满足要求才会输出结果

js
// 这个函数的入参个数是固定的
function add(a, b, c, d, e) {
  return a + b + c + d + e;
}
// 柯理化函数
function curring(fn, arg = []) {
  let len = fn.length; // 实际需要的参数个数
  return (...args) => {
    let arr = [...arg, ...args]; // 我们要收集的每次传入的参数
    if (arr.length === len) {
      return fn(...arr);
    }
    return curring(fn, arr);
  };
}
// 柯理化函数,每次入参都是一个参数
let r = curring(add)(1)(2)(3)(4)(5); // 15
// 偏函数,每次入参个数不定
let p = curring(add)(1, 2)(3, 4, 5); // 15

柯理化类型校验函数

js
// 柯理化函数
function curring(fn, arg = []) {
  let len = fn.length; // 实际需要的参数个数
  return (...args) => {
    let arr = [...arg, ...args]; // 我们要收集的每次传入的参数
    if (arr.length === len) {
      return fn(...arr);
    }
    return curring(fn, arr);
  };
}

// 校验类型方法
function isType(typeing, val) {
  return Object.prototype.toString.call(val) === `[object ${typeing}]`;
}

let utils = {};
["String", "Number", "Boolean"].forEach((typeing) => {
  utils["is" + typeing] = curring(isType)(typeing);
});

utils.isString(123); // true
utils.isBoolean(true); // true

1.4、after函数

应用场景:有一堆异步函数在执行(ajax),我们想在这堆异步函数全部执行完成后进行某项操作,比如进行页面渲染,但是,异步函数返回结果的时间是不确定的,这时,after就派上用场了。

具体实现:

js
const fs = require('fs');
fs.readFile("./a.txt", "utf8", (err, data) => {
  console.log("a.txt读取完毕");
  fn();
});

fs.readFile("./b.txt", "utf8", (err, data) => {
  console.log("b.txt读取完毕");
  fn();
});

// 这里的2表示有两个异步方法,当after被调用了两次后就会执行回调函数
let fn = after(2, () => {
  console.log("文件读取完毕");
  // todo...
});

// after核心方法
function after(timer, callback) {
  return function () {
    if (--timer === 0) {
      callback();
    }
  };
}

二. 常用类型检测

1. 判断可迭代对象

例如:数组、字符串...

js
function isIterable(val) {
  return val !== null && typeof val[Symbol.iterator] === "function";
}

2. 判断Promise

如果一个变量是 Object, 有 then 和 catch 方法, 就认为是 Promise

js
function isPromise(val) {
  return (
    val !== null &&
    typeof val === "object" &&
    typeof val.then === "function" &&
    typeof val.catch === "function"
  );
}

三.其他

判断当前环境的node版本是否符合预期

前提, 项目中的package.json文件中应该存在engines节点

json
{
  "name": "demo",
  "version": "1.0.0",
.......
  "engines": {
    "node": "^12.0.0 || >= 14.0.0"
  }
}

具体检测方法

js
const semver = require("semver"); // 
const chalk = require("chalk"); // 粉笔

// 检查Nodejs版本号
function checkNodeVersion(wanted, id) {
  if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
    console.log(
      chalk.red(
        "You are using Node " +
          process.version +
          ", but this version of " +
          id +
          " requires Node " +
          wanted +
          ".\nPlease upgrade your Node version."
      )
    );
    process.exit(1);
  }
}

/* 使用 */
const requiredVersion = require("../package.json").engines.node;
checkNodeVersion(requiredVersion, "demo");

四. 防抖函数 (debounce)

防抖函数的作用是在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。常用于搜索框输入、窗口 resize、按钮点击等场景。

4.1、基础版防抖函数

js
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }
    // 设置新的定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

使用示例:

js
// 搜索框输入防抖
const searchInput = document.getElementById("search");
const handleSearch = debounce(function (e) {
  console.log("搜索:", e.target.value);
  // 执行搜索逻辑...
}, 500);

searchInput.addEventListener("input", handleSearch);

4.2、增强版防抖函数(支持立即执行)

支持 leading(立即执行)和 trailing(延迟执行)选项:

js
function debounce(fn, delay, options = {}) {
  let timer = null;
  let leadingTimer = null;
  const { leading = false, trailing = true } = options;

  return function (...args) {
    const context = this;
    const callNow = leading && !timer;

    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 如果设置了 leading,且当前没有定时器,立即执行
    if (callNow) {
      fn.apply(context, args);
      // 设置一个标志,防止在 delay 时间内再次立即执行
      timer = setTimeout(() => {
        timer = null;
      }, delay);
    } else if (trailing) {
      // 延迟执行
      timer = setTimeout(() => {
        fn.apply(context, args);
        timer = null;
      }, delay);
    }
  };
}

使用示例:

js
// 立即执行版本(leading: true)
const handleClick = debounce(
  function () {
    console.log("按钮被点击");
  },
  1000,
  { leading: true, trailing: false }
);

// 延迟执行版本(默认,trailing: true)
const handleResize = debounce(
  function () {
    console.log("窗口大小改变");
  },
  300,
  { trailing: true }
);

4.3、完整版防抖函数(支持取消和立即执行)

js
function debounce(fn, delay, options = {}) {
  let timer = null;
  let result;
  const { leading = false, trailing = true } = options;

  const debounced = function (...args) {
    const context = this;
    const callNow = leading && !timer;

    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 如果设置了 leading,且当前没有定时器,立即执行
    if (callNow) {
      result = fn.apply(context, args);
      // 设置一个标志,防止在 delay 时间内再次立即执行
      timer = setTimeout(() => {
        timer = null;
      }, delay);
    } else if (trailing) {
      // 延迟执行
      timer = setTimeout(() => {
        result = fn.apply(context, args);
        timer = null;
      }, delay);
    }

    return result;
  };

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

  // 立即执行
  debounced.flush = function (...args) {
    if (timer) {
      clearTimeout(timer);
      timer = null;
      return fn.apply(this, args);
    }
    return result;
  };

  return debounced;
}

使用示例:

js
const handleSearch = debounce(function (keyword) {
  console.log("搜索:", keyword);
  return `搜索结果: ${keyword}`;
}, 500);

// 正常使用
handleSearch("JavaScript");

// 取消防抖
handleSearch.cancel();

// 立即执行
const result = handleSearch.flush("TypeScript");
console.log(result);

4.4、应用场景

  1. 搜索框输入:用户输入停止 500ms 后再执行搜索
  2. 窗口 resize:窗口大小改变停止 300ms 后再执行回调
  3. 按钮点击:防止用户快速重复点击
  4. 滚动事件:滚动停止后再执行某些操作
  5. 表单验证:用户输入停止后再进行验证

五. 节流函数 (throttle)

节流函数用于限制高频事件在固定时间窗口内只执行一次,常用于滚动、窗口大小改变、按钮快速点击等高频操作,避免频繁触发造成的性能问题。

5.1、时间戳版节流(leading 触发)

js
function throttle(fn, wait) {
  let lastInvokeTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastInvokeTime >= wait) {
      lastInvokeTime = now;
      return fn.apply(this, args);
    }
  };
}

特点:进入时立即执行一次,停止触发后不再补尾。

5.2、定时器版节流(trailing 触发)

js
function throttle(fn, wait) {
  let timer = null;
  return function (...args) {
    if (timer) return;
    const context = this;
    timer = setTimeout(() => {
      timer = null;
      fn.apply(context, args);
    }, wait);
  };
}

特点:进入时不立即执行,停止触发后补执行一次。

5.3、可配置版节流(支持 leading/trailing、取消与立即执行)

js
function throttle(fn, wait, options = {}) {
  let timer = null;
  let lastInvokeTime = 0;
  let leadingResult;
  const { leading = true, trailing = true } = options;

  const later = (context, args) => {
    timer = null;
    lastInvokeTime = Date.now();
    return fn.apply(context, args);
  };

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

    if (!lastInvokeTime && leading === false) {
      lastInvokeTime = now;
    }

    const remaining = wait - (now - lastInvokeTime);

    if (remaining <= 0 || remaining > wait) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastInvokeTime = now;
      leadingResult = fn.apply(context, args);
      return leadingResult;
    }

    if (!timer && trailing !== false) {
      timer = setTimeout(() => later(context, args), remaining);
    }
    return leadingResult;
  };

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

  throttled.flush = function (...args) {
    if (timer) {
      clearTimeout(timer);
      const context = this;
      timer = null;
      lastInvokeTime = Date.now();
      return fn.apply(context, args);
    }
    return leadingResult;
  };

  return throttled;
}

使用示例:

js
// 滚动事件:每 200ms 触发一次
const onScroll = throttle(() => {
  console.log('scroll', window.scrollY);
}, 200);
window.addEventListener('scroll', onScroll);

// 配置版:进入时执行,停止触发后也补一次
const onResize = throttle(() => {
  console.log('resize');
}, 300, { leading: true, trailing: true });
window.addEventListener('resize', onResize);

// 取消与立即执行
onResize.cancel();
onResize.flush();

5.4、选择建议

  1. 高频 UI 反馈(按钮点击):时间戳版(leading)
  2. 日志/上报等收敛:定时器版(trailing)
  3. 需要首尾皆可控:可配置版(leading/trailing)