文章目录
- 前端高频得手写题
-
-
- [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);
};
}
- 核心逻辑:短时间内多次触发事件,仅执行最后一次,中间触发全部重置计时。
- 闭包作用:timer 被内部函数引用,变量常驻内存,实现计时累加。
this & 参数:使用 apply 保留原函数 this 指向,...args 接收事件参数。 - 使用场景:搜索框联想、输入监听、窗口 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);
}
};
}
解析
- 核心逻辑:固定时间间隔内,只执行一次,稀释高频触发频率。
- 原理:通过时间戳对比,限制单位时间执行次数。
- 使用场景:页面滚动 scroll、鼠标移动 mousemove、按钮高频点击、拖拽。
- 和防抖区别:防抖 "等最后一次",节流 "匀速执行"。
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;
}
解析
- 浅拷贝问题:只复制第一层,引用类型依旧共用内存地址,改值互相影响。
- 深拷贝:递归遍历所有层级,创建全新对象,完全独立。
- 循环引用:用 WeakMap 缓存已拷贝对象,避免递归死循环。
- 特殊类型:单独处理 Date、RegExp,防止拷贝后失效。
4. 数组去重
javascript
function unique(arr) {
// Set 集合元素不重复,再转回数组
return [...new Set(arr)];
}
解析
- Set 是 ES6 集合,值唯一、自动去重。
- 扩展运算符 ... 将 Set 结构转为普通数组。
- 局限性:无法去重引用类型(对象、数组),基础类型首选此写法。
5. 数组扁平化(多维数组转一维)
javascript
function flat(arr) {
// 递归 + 归并
return arr.reduce((prev, curr) => {
// 当前项是数组则递归,否则直接拼接
return prev.concat(Array.isArray(curr) ? flat(curr) : curr);
}, []);
}
解析
- 利用 reduce 累加遍历,concat 实现数组合并。
- 递归判断元素是否为数组,层层拆解多维结构。
- 原生 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;
};
解析
- call 作用:改变函数 this 指向,参数逐个传入。
- 原理:把函数临时挂载到目标对象上,对象调用函数 → this 自然指向该对象。
- 使用 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;
};
解析
- 和 call 逻辑几乎一致,唯一区别:apply 第二个参数为数组。
- 兼容参数为空的情况,默认设空数组。
8. 手写 bind
javascript
Function.prototype.myBind = function (context, ...args1) {
const fn = this;
// bind 返回一个新函数
return function (...args2) {
// 合并两次参数,借用 call 改变 this
return fn.myCall(context, ...args1, ...args2);
};
};
解析
- bind 特点:不会立即执行,返回新函数,可延迟调用。
- 支持参数分两次传递:绑定时传一部分,调用时再传剩余。
- 底层复用 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;
}
解析
- 严格复刻 new 四步流程:
- 生成空实例对象;
- 关联原型链;
- 执行构造函数,绑定 this;
- 判断返回值:引用类型优先返回自身,基本类型返回实例。
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;
}
解析
- instanceof 作用:判断实例是否属于某个构造函数。
- 原理:逐级遍历原型链,对比原型对象。
- 终止条件:原型链顶端为 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);
}
}
解析
- 前端常用跨组件通信方案(Vue 事件总线核心)。
- 三大方法:on 订阅、emit 发布、off 取消订阅。
- 核心结构:用对象存储「事件名 + 回调队列」。
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);
});
});
});
};
解析
- 规则:全部成功才成功,一个失败则整体失败。
- 用计数器判断是否全部执行完毕,按原数组顺序保存结果。
- 入参不一定是 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);
});
});
};
解析
- 规则:赛跑机制,第一个完成的 Promise 决定最终状态。
- 常用场景:接口超时拦截、竞态请求。
14. 函数柯里化
javascript
function currying(fn) {
const curried = (...args) => {
// 参数数量 >= 原函数形参总数,执行原函数
if (args.length >= fn.length) {
return fn(...args);
}
// 参数不足,继续接收新参数
return (...newArgs) => curried(...args, ...newArgs);
};
return curried;
}
解析
- 柯里化:把多参数函数,拆分为多个单参数函数,分步传参。
- 作用:参数复用、延迟执行、逻辑拆分。
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;
};
解析
- map 遍历数组,执行回调,返回新数组,不修改原数组。
- 必须透传 当前元素、索引、原数组 三个参数,符合原生规范。
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;
};
解析
- reduce 核心:累加器,迭代汇总数组数据。
- 区分「有无初始值」两种情况,对应不同遍历起点。
- 回调依次传入:累加值、当前元素、索引、原数组。