前端高频得手写题

文章目录

  • 前端高频得手写题
      • [1. 防抖 debounce](#1. 防抖 debounce)
      • [2. 节流 throttle](#2. 节流 throttle)
      • [3. 深拷贝 deepClone](#3. 深拷贝 deepClone)
      • [4. 数组去重](#4. 数组去重)
      • [5. 数组扁平化(多维数组转一维)](#5. 数组扁平化(多维数组转一维))
      • [6. 手写 call](#6. 手写 call)
      • [7. 手写 apply](#7. 手写 apply)
      • [8. 手写 bind](#8. 手写 bind)
      • [9. 手写 new](#9. 手写 new)
      • [10. 手写 instanceof](#10. 手写 instanceof)
      • [11. 发布订阅 EventBus](#11. 发布订阅 EventBus)
      • [12. 手写 Promise.all](#12. 手写 Promise.all)
      • [13. 手写 Promise.race](#13. 手写 Promise.race)
      • [14. 函数柯里化](#14. 函数柯里化)
      • [15. 手写 String.prototype.trim(去除首尾空格)](#15. 手写 String.prototype.trim(去除首尾空格))
      • [16. 手写 Array.prototype.map](#16. 手写 Array.prototype.map)
      • [17. 手写 Array.prototype.reduce](#17. 手写 Array.prototype.reduce)

前端高频得手写题

1. 防抖 debounce

javascript 复制代码
function debounce(fn, delay = 300) {
  let timer = null; // 定时器标识,闭包保留
  // 返回新函数
  return function (...args) {
    // 每次触发都清空上一次定时器,重置计时
    clearTimeout(timer);
    // 重新开启延时执行
    timer = setTimeout(() => {
      // 修正this、透传参数
      fn.apply(this, args);
    }, delay);
  };
}
  1. 核心逻辑:短时间内多次触发事件,仅执行最后一次,中间触发全部重置计时。
  2. 闭包作用:timer 被内部函数引用,变量常驻内存,实现计时累加。
    this & 参数:使用 apply 保留原函数 this 指向,...args 接收事件参数。
  3. 使用场景:搜索框联想、输入监听、窗口 resize、鼠标移入移出。

2. 节流 throttle

javascript 复制代码
function throttle(fn, interval = 300) {
  let lastTime = 0; // 记录上一次执行时间
  return function (...args) {
    const now = Date.now();
    // 当前时间 - 上次执行时间 >= 间隔,才允许执行
    if (now - lastTime >= interval) {
      lastTime = now; // 更新执行时间
      fn.apply(this, args);
    }
  };
}

解析

  1. 核心逻辑:固定时间间隔内,只执行一次,稀释高频触发频率。
  2. 原理:通过时间戳对比,限制单位时间执行次数。
  3. 使用场景:页面滚动 scroll、鼠标移动 mousemove、按钮高频点击、拖拽。
  4. 和防抖区别:防抖 "等最后一次",节流 "匀速执行"。

3. 深拷贝 deepClone

javascript 复制代码
function deepClone(obj, map = new WeakMap()) {
  // 1. 基本类型 / null 直接返回
  if (obj === null || typeof obj !== "object") return obj;

  // 2. 处理循环引用:已拷贝过直接取出
  if (map.has(obj)) return map.get(obj);

  // 3. 处理特殊引用类型:日期、正则
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);

  // 4. 判断数组/普通对象,创建新容器
  const newObj = Array.isArray(obj) ? [] : {};

  // 5. 存入映射表,标记已拷贝
  map.set(obj, newObj);

  // 6. 递归遍历所有属性
  for (let key in obj) {
    // 只拷贝自身属性,排除原型上属性
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepClone(obj[key], map);
    }
  }
  return newObj;
}

解析

  1. 浅拷贝问题:只复制第一层,引用类型依旧共用内存地址,改值互相影响。
  2. 深拷贝:递归遍历所有层级,创建全新对象,完全独立。
  3. 循环引用:用 WeakMap 缓存已拷贝对象,避免递归死循环。
  4. 特殊类型:单独处理 Date、RegExp,防止拷贝后失效。

4. 数组去重

javascript 复制代码
function unique(arr) {
  // Set 集合元素不重复,再转回数组
  return [...new Set(arr)];
}

解析

  1. Set 是 ES6 集合,值唯一、自动去重。
  2. 扩展运算符 ... 将 Set 结构转为普通数组。
  3. 局限性:无法去重引用类型(对象、数组),基础类型首选此写法。

5. 数组扁平化(多维数组转一维)

javascript 复制代码
function flat(arr) {
  // 递归 + 归并
  return arr.reduce((prev, curr) => {
    // 当前项是数组则递归,否则直接拼接
    return prev.concat(Array.isArray(curr) ? flat(curr) : curr);
  }, []);
}

解析

  1. 利用 reduce 累加遍历,concat 实现数组合并。
  2. 递归判断元素是否为数组,层层拆解多维结构。
  3. 原生 API:arr.flat(Infinity) 可直接无限扁平化,手写考察递归思想。

6. 手写 call

javascript 复制代码
Function.prototype.myCall = function (context, ...args) {
  // 1. 绑定默认上下文,非对象指向 window
  context = context || window;
  // 2. 用 Symbol 创建唯一属性,防止属性名冲突
  const fn = Symbol();
  // 3. 将原函数挂载到上下文对象上
  context[fn] = this;
  // 4. 执行函数,此时 this 指向 context
  const result = context[fn](...args);
  // 5. 删除临时属性,还原对象
  delete context[fn];
  // 6. 返回执行结果
  return result;
};

解析

  1. call 作用:改变函数 this 指向,参数逐个传入。
  2. 原理:把函数临时挂载到目标对象上,对象调用函数 → this 自然指向该对象。
  3. 使用 Symbol 保证属性唯一,避免覆盖对象原有属性。

7. 手写 apply

javascript 复制代码
Function.prototype.myApply = function (context, args = []) {
  context = context || window;
  const fn = Symbol();
  context[fn] = this;
  // apply 第二个参数是数组,解构传参
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

解析

  1. 和 call 逻辑几乎一致,唯一区别:apply 第二个参数为数组。
  2. 兼容参数为空的情况,默认设空数组。

8. 手写 bind

javascript 复制代码
Function.prototype.myBind = function (context, ...args1) {
  const fn = this;
  // bind 返回一个新函数
  return function (...args2) {
    // 合并两次参数,借用 call 改变 this
    return fn.myCall(context, ...args1, ...args2);
  };
};

解析

  1. bind 特点:不会立即执行,返回新函数,可延迟调用。
  2. 支持参数分两次传递:绑定时传一部分,调用时再传剩余。
  3. 底层复用 call 实现 this 绑定。

9. 手写 new

javascript 复制代码
function myNew(Constructor, ...args) {
  // 1. 创建空对象
  const obj = {};
  // 2. 空对象原型 指向 构造函数原型
  obj.__proto__ = Constructor.prototype;
  // 3. 改变 this 并执行构造函数
  const res = Constructor.apply(obj, args);
  // 4. 如果返回引用类型,直接返回该值;否则返回新建对象
  return res instanceof Object ? res : obj;
}

解析

  1. 严格复刻 new 四步流程:
  2. 生成空实例对象;
  3. 关联原型链;
  4. 执行构造函数,绑定 this;
  5. 判断返回值:引用类型优先返回自身,基本类型返回实例。

10. 手写 instanceof

javascript 复制代码
function myInstanceof(left, right) {
  // 获取实例的隐式原型
  let proto = Object.getPrototypeOf(left);
  // 沿着原型链向上遍历
  while (proto) {
    // 找到构造函数原型,返回 true
    if (proto === right.prototype) return true;
    // 继续向上查找
    proto = Object.getPrototypeOf(proto);
  }
  // 遍历到 null 还没找到,返回 false
  return false;
}

解析

  1. instanceof 作用:判断实例是否属于某个构造函数。
  2. 原理:逐级遍历原型链,对比原型对象。
  3. 终止条件:原型链顶端为 null,遍历结束。

11. 发布订阅 EventBus

javascript 复制代码
class EventBus {
  constructor() {
    // 存储事件:键=事件名,值=回调数组
    this.events = {};
  }

  // 订阅事件:on
  on(name, callback) {
    if (!this.events[name]) this.events[name] = [];
    this.events[name].push(callback);
  }

  // 触发事件:emit
  emit(name, ...args) {
    // 遍历执行所有回调,透传参数
    this.events[name]?.forEach(cb => cb(...args));
  }

  // 取消订阅:off
  off(name, callback) {
    if (!this.events[name]) return;
    // 过滤掉指定回调
    this.events[name] = this.events[name].filter(cb => cb !== callback);
  }
}

解析

  1. 前端常用跨组件通信方案(Vue 事件总线核心)。
  2. 三大方法:on 订阅、emit 发布、off 取消订阅。
  3. 核心结构:用对象存储「事件名 + 回调队列」。

12. 手写 Promise.all

javascript 复制代码
Promise.myAll = function (promiseList) {
  return new Promise((resolve, reject) => {
    const result = [];
    let count = 0;
    const len = promiseList.length;

    // 空数组直接成功
    if (len === 0) resolve([]);

    promiseList.forEach((p, idx) => {
      // 转为标准 Promise
      Promise.resolve(p).then(res => {
        result[idx] = res;
        count++;
        // 所有 Promise 执行完成,统一返回结果
        if (count === len) resolve(result);
      }).catch(err => {
        // 一个失败,整体直接失败
        reject(err);
      });
    });
  });
};

解析

  1. 规则:全部成功才成功,一个失败则整体失败。
  2. 用计数器判断是否全部执行完毕,按原数组顺序保存结果。
  3. 入参不一定是 Promise,用 Promise.resolve 统一转换。

13. 手写 Promise.race

javascript 复制代码
Promise.myRace = function (promiseList) {
  return new Promise((resolve, reject) => {
    promiseList.forEach(p => {
      // 谁先完成(成功/失败),就采用谁的结果
      Promise.resolve(p)
        .then(resolve)
        .catch(reject);
    });
  });
};

解析

  1. 规则:赛跑机制,第一个完成的 Promise 决定最终状态。
  2. 常用场景:接口超时拦截、竞态请求。

14. 函数柯里化

javascript 复制代码
function currying(fn) {
  const curried = (...args) => {
    // 参数数量 >= 原函数形参总数,执行原函数
    if (args.length >= fn.length) {
      return fn(...args);
    }
    // 参数不足,继续接收新参数
    return (...newArgs) => curried(...args, ...newArgs);
  };
  return curried;
}

解析

  1. 柯里化:把多参数函数,拆分为多个单参数函数,分步传参。
  2. 作用:参数复用、延迟执行、逻辑拆分。

15. 手写 String.prototype.trim(去除首尾空格)

javascript 复制代码
String.prototype.myTrim = function () {
  // 正则:^\s+ 匹配开头空格,\s+$ 匹配结尾空格
  return this.replace(/^\s+|\s+$/g, "");
};

解析

正则 \s 匹配所有空白符(空格、制表符、换行)。

g 全局匹配,一次性清除首尾所有空格。

16. 手写 Array.prototype.map

javascript 复制代码
Array.prototype.myMap = function (callback) {
  const res = [];
  // this 指向调用的原数组
  for (let i = 0; i < this.length; i++) {
    // 回调参数:元素、下标、原数组
    res.push(callback(this[i], i, this));
  }
  return res;
};

解析

  1. map 遍历数组,执行回调,返回新数组,不修改原数组。
  2. 必须透传 当前元素、索引、原数组 三个参数,符合原生规范。

17. 手写 Array.prototype.reduce

javascript 复制代码
Array.prototype.myReduce = function (callback, initial) {
  let acc = initial;
  let startIndex = 0;

  // 无初始值,取数组第一个元素作为累加器初始值
  if (initial === undefined) {
    acc = this[0];
    startIndex = 1;
  }

  for (let i = startIndex; i < this.length; i++) {
    // 执行回调,更新累加器
    acc = callback(acc, this[i], i, this);
  }
  return acc;
};

解析

  1. reduce 核心:累加器,迭代汇总数组数据。
  2. 区分「有无初始值」两种情况,对应不同遍历起点。
  3. 回调依次传入:累加值、当前元素、索引、原数组。
相关推荐
初一初十2 小时前
vue3实现的纯前端护肤品商城网站
前端·javascript·vue.js·前端框架
卷帘依旧2 小时前
React状态管理方案怎么选
前端
zeqinjie2 小时前
Flutter 折叠屏 iPad / 宽屏适配实践
android·前端·flutter
小村儿2 小时前
连载13- 内部Tools,Claude Code 怎么真正"动"你的代码
前端·后端·ai编程
IT_陈寒2 小时前
Python的线程池把我坑惨了,原来异步不是万能的
前端·人工智能·后端
初一初十3 小时前
vue3茶叶商城网站vue网页vuejs前端
前端·javascript·vue.js·vscode·前端框架
kyriewen3 小时前
前端性能优化:LCP 从 4s 到 0.9s 的 5 个核心手段(附配置代码)
前端·javascript·性能优化
xiaofeichaichai3 小时前
Proxy与Reflect
前端·javascript
小蜜蜂dry4 小时前
nestjs实战-权限二:角色模块
前端·后端·nestjs