Skip to content

前端常见手写代码

防抖

用于用户频繁触发某个事件(如输入搜索),避免函数被频繁执行,最近一次触发需要等待一定的时间间隔才会被执行,如果期间再次触发执行,则重置定时器,直到停止触发等待足够时间

js
function debounce(fn, delay) {
  var timer;

  return function () {
    clearTimeout(timer);
    timer = setTimeout(fn.bind(this, ...arguments), delay);
  };
}

// 测试
function task() {
  console.log('run task', arguments);
}
const debounceTask = debounce(task, 1000);
window.addEventListener('scroll', debounceTask);

首次立即执行

js
function debounce(fn, delay) {
  var timer = null;
  var first = true;

  return function () {
    clearTimeout(timer);
    const f = fn.bind(this, ...arguments);

    if (first) {
      f();
      first = false;
    } else {
      timer = setTimeout(f, delay);
    }
  };
}

节流

用于限制函数执行频率

javascript
function throttle(fn, delay) {
  var last = 0;
  var timer;

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

    clearTimeout(timer);
    if (now - last >= delay) {
      last = now;
      fn.apply(this, arguments);
    } else {
      timer = setTimeout(() => {
        fn.apply(this, arguments);
        last = Date.now();
      }, delay - (now - last));
    }
  };
}

深拷贝

JSON 方法

javascript
// 不支持值为 undefined、函数和循环引用的情况
const cloneObj = JSON.parse(JSON.stringify(obj));

递归拷贝

js
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  const copy = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key]);
    }
  }

  return copy;
}

stringify 方法实现

js
function stringify(obj) {
  if (obj === null || typeof obj !== 'object') {
    // 数组 undefined 会被转换成 null
    return obj === undefined || obj === null ? 'null' : obj;
  }
  if (Array.isArray(obj)) {
    return '[' + json.map((m) => stringify(m)).join(',') + ']';
  }

  return (
    '{' +
    Object.keys(obj)
      // 过滤值为 undefined 的 key
      .filter((key) => obj[key] !== undefined)
      .map((key) => `"${key}":${stringify(obj[key])}`)
      .join(',') +
    '}'
  );
}

继承

ES5 继承

组合继承

javascript
function Parent(value) {
  this.val = value;
}
Parent.prototype.getValue = function () {
  console.log(this.val);
};
function Child(value) {
  Parent.call(this, value);
}
Child.prototype = new Parent();
  • 缺点:原型上创建了父类属性(调用父类构造函数导致)

寄生组合继承

javascript
function Parent(value) {
  this.val = value;
}
Parent.prototype.getValue = function () {
  console.log(this.val);
};

function Child(value) {
  Parent.call(this, value);
}
Child.prototype = Object.create(Parent.prototype, {
  // 原型构造函数指向Child
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true,
  },
});

ES6 继承

javascript
class Parent {
  constructor(value) {
    this.val = value;
  }
  getValue() {
    console.log(this.val);
  }
}
class Child extends Parent {
  constructor(value) {
    super(value);
  }
}

数组去重

Set

javascript
[...new Set([1, 3, 4, 5, 3, 1])];

Map

javascript
function uniq(arr) {
  var visited = {};
  var res = [];

  for (e of arr) {
    if (visited[e]) continue;
    res.push(e);
    visited[e] = true;
  }
  return res;
}

虚拟滚动

tsx
import React, { useState, useCallback, useRef, useEffect } from 'react';

export interface VirtualScrollProps {
  items: string[];
  height: number;
  rowHeight: number;
}

export default function VirtualScroll({ items, height, rowHeight }: VirtualScrollProps) {
  // 保存滚动条位置的状态
  const [scrollTop, setScrollTop] = useState(0);
  // 获取列表容器元素的引用
  const containerRef = useRef<HTMLDivElement>(null);
  // 计算可见区域的行数
  const visibleRowCount = Math.ceil(height / rowHeight);

  // 监听滚动条的滚动事件,更新滚动条位置的状态
  const handleScroll = useCallback(() => {
    const scrollTop = containerRef.current!.scrollTop;
    setScrollTop(scrollTop);
  }, []);

  // 使用 useEffect 添加滚动条滚动事件的监听器,并在组件卸载时移除监听器
  useEffect(() => {
    const container = containerRef.current;
    container!.addEventListener('scroll', handleScroll);
    return () => {
      container!.removeEventListener('scroll', handleScroll);
    };
  }, [handleScroll]);

  // 计算可见区域的起始行和结束行
  const start = Math.floor(scrollTop / rowHeight);
  const end = Math.min(start + visibleRowCount + 1, items.length);

  // 渲染可见区域内的元素
  const visibleItems = items.slice(start, end).map((item, index) => (
    <div key={start + index} style={{ height: rowHeight }}>
      {item}
    </div>
  ));

  // 计算列表容器的 paddingTop 和 paddingBottom,确保可见区域内的元素能够正确地垂直对齐
  const paddingTop = start * rowHeight;
  const paddingBottom = (items.length - end) * rowHeight;

  return (
    <div ref={containerRef} style={{ height, overflowY: 'scroll' }}>
      {/* 使用 paddingTop 和 paddingBottom 让可见区域内的元素能够正确地垂直对齐 */}
      <div style={{ paddingTop, paddingBottom }}>{visibleItems}</div>
    </div>
  );
}

限制最大并发数

js
function limit(maxCount) {
  return {
    count: 0,
    queue: [],
    pauseState: false,
    push(task) {
      this.queue.push(task);
      this.next();
    },
    next() {
      if (this.queue.length && this.count < maxCount) {
        this.count++;
        this.queue
          .shift()()
          .finally(() => {
            this.count--;
            if (!this.pauseState) {
              this.next();
            }
          });
      }
    },
    pause() {
      this.pauseState = true;
    },
    start() {
      this.pauseState = false;
      let max = Math.min(this.queue.length, maxCount - this.count);

      for (let i = 0; i < max; i++) {
        this.next();
      }
    },
  };
}

// 测试代码
const scheduler = limit(2);

function addTask(time, order) {
  scheduler.push(() =>
    new Promise((resolve) => setTimeout(resolve, time)).then(() => console.log(order)),
  );
}

addTask(1000, 1);
addTask(500, 2);
addTask(300, 3);
addTask(400, 4);
// 2 3 1 4 耗时 1200ms

延迟调用

js
function lazyMan(name) {
  let runing = false;
  const sleep = (time) => () =>
    new Promise((resolve) => {
      console.log(`Wake up after ${time}`);
      setTimeout(resolve, time * 1000);
    });
  const obj = {
    tasks: [],
    async next() {
      const task = this.tasks.shift();

      if (task) {
        await task();
        this.next();
      }
    },
    run() {
      Promise.resolve().then(() => {
        if (runing) {
          return;
        }
        runing = true;
        this.next();
      });
    },
    eat(food) {
      this.tasks.push(() => console.log(`Eat ${food}`));
      this.run();
      return this;
    },
    sleep(time) {
      this.tasks.push(sleep(time));
      this.run();
      return this;
    },
    sleepFirst(time) {
      this.tasks.unshift(sleep(time));
      this.run();
      return this;
    },
  };

  obj.tasks.push(() => console.log(`Hi! This is ${name}!`));
  obj.run();

  return obj;
}

lazyMan('Hank').sleep(10).eat('dinner');
// Hi! This is Hank!
// Wake up after 10
// Eat dinner
lazyMan('Hank').eat('dinner').sleepFirst(10);
// Wake up after 10
// Hi! This is Hank!
// Eat dinner