前端面试:6大高频题详解(洗牌/防抖/深拷贝)
前端面试里,手写代码是考察JavaScript基础、逻辑思维和工程意识的核心环节。本文整理6道高频手写真题,覆盖数组操作、网络请求、性能优化、数据拷贝四大方向,从问题分析、原理讲解、代码实现、避坑指南一步步拆解,适合面试复盘、日常查漏补缺。
💡 前言:手写代码的考察重点
面试官看手写代码,不止看能不能写出来,更看重这三点:
- 基础扎实度:闭包、作用域、类型判断、递归等核心语法的运用
- 边界思维:空值、异常、特殊类型、兼容性等场景的处理
- 代码规范:可读性、鲁棒性、简洁度,不写冗余、易出错的代码
一、数组乱序:Fisher-Yates 洗牌算法
📌 题目要求
实现函数随机打乱数组,保证每个元素出现在各位置概率均等,兼顾性能,不随意修改原数组。
示例:输入
[1,2,3]→ 输出可为[2,1,3]、[3,1,2]等随机结果
🧠 核心原理
采用 Fisher-Yates 洗牌算法,时间复杂度 O(n),空间复杂度 O(1)(原地洗牌),是业界公认的均匀洗牌方案:
- 从数组末尾向前遍历,减少重复交换
- 每次生成
[0, 当前索引]范围内的随机下标 - 交换当前元素与随机位置元素,保证随机性
避坑提示:不建议用 sort(() => Math.random() - 0.5) 打乱数组,该方式概率分布不均,属于面试常见易错点。
✅ 代码实现
/**
* 数组洗牌(Fisher-Yates 算法)
* @param {Array} arr - 原始数组
* @param {boolean} isMutate - 是否修改原数组,默认false
* @returns {Array} 乱序后的数组
*/
function getArrRandom(arr, isMutate = false) {
// 参数校验
if (!Array.isArray(arr)) throw new Error('参数必须为数组');
// 非原地洗牌,避免副作用
const newArr = isMutate ? arr : [...arr];
const len = newArr.length;
for (let i = len - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1));
// 解构交换
[newArr[randomIndex], newArr[i]] = [newArr[i], newArr[randomIndex]];
}
return newArr;
}
// 测试
const arr = [1,2,3,4,5];
console.log(getArrRandom(arr));
console.log(arr); // 原数组不变
❌ 常见易错点
- 随机索引范围计算错误,导致乱序不均匀
- 直接修改原数组,产生意外副作用
- 缺少参数类型校验,代码鲁棒性差
二、原生 AJAX 封装
📌 题目要求
手写原生AJAX,支持GET异步请求,兼容旧版IE,做好异常处理,通过回调返回结果。
🧠 核心原理
基于 XMLHttpRequest 对象实现异步通信,监听请求状态变化,区分成功、失败场景,兼容IE低版本,做好参数校验和异常捕获。
✅ 代码实现
/**
* 原生AJAX(GET异步)
* @param {string} method - 请求方法
* @param {string} url - 请求地址
* @param {Function} callback - 回调函数 (err, data)
*/
function ajax(method, url, callback) {
// 参数校验
if (typeof method !== 'string' || typeof url !== 'string') {
throw new Error('method和url需为字符串类型');
}
if (typeof callback !== 'function') {
throw new Error('callback需为函数类型');
}
// 兼容创建XHR实例
let xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
// 初始化请求
const upperMethod = method.toUpperCase();
xhr.open(upperMethod, url, true);
xhr.send(null);
// 状态监听
xhr.onreadystatechange = function() {
if (!xhr) return;
if (xhr.readyState === 4) {
try {
if (xhr.status === 200) {
callback(null, xhr.responseText);
} else {
const errMsg = xhr.status === 0 ? '网络异常或跨域拦截' : `请求失败,状态码:${xhr.status}`;
callback(new Error(errMsg), null);
}
} catch (e) {
callback(new Error(`响应处理失败:${e.message}`), null);
} finally {
xhr = null;
}
}
};
}
// 测试
ajax('GET', 'https://jsonplaceholder.typicode.com/todos/1', (err, data) => {
if (err) return console.error('请求失败', err);
console.log('请求成功', JSON.parse(data));
});
❌ 常见易错点
- 忽略IE兼容性,未做
ActiveXObject兼容处理 - 未处理状态码为0的网络异常场景
- 缺少参数校验,回调逻辑无容错
三、深拷贝(递归+特殊类型+循环引用)
📌 题目要求
实现深拷贝函数,完整复制对象层级,不共享内存,支持Date、RegExp等特殊类型,解决循环引用问题。
🧠 核心原理
- 基本类型直接返回,引用类型递归拷贝
- 特殊对象(Date/RegExp)新建实例继承属性
- WeakMap缓存已拷贝对象,避免循环引用导致死循环
✅ 代码实现
/**
* 深拷贝(支持Date/RegExp/循环引用)
* @param {*} obj - 待拷贝数据
* @param {WeakMap} cache - 缓存已拷贝对象
* @returns {*} 拷贝结果
*/
function deepClone(obj, cache = new WeakMap()) {
// 基本类型直接返回
if (typeof obj !== 'object' || obj === null) return obj;
// 处理循环引用
if (cache.has(obj)) return cache.get(obj);
let result;
// 特殊类型处理
if (obj instanceof Date) {
result = new Date(obj);
cache.set(obj, result);
return result;
}
if (obj instanceof RegExp) {
result = new RegExp(obj.source, obj.flags);
cache.set(obj, result);
return result;
}
// 初始化容器
result = Array.isArray(obj) ? [] : {};
cache.set(obj, result);
// 递归拷贝自身属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key], cache);
}
}
return result;
}
// 测试
const obj = {
a: 1,
b: [2, 3],
c: new Date(),
d: /test/g
};
obj.self = obj; // 循环引用
const newObj = deepClone(obj);
console.log(newObj.b === obj.b); // false
console.log(newObj.self === newObj); // true
❌ 常见易错点
- 遗漏Date、RegExp等特殊类型,拷贝后丢失原有特性
- 未处理循环引用,递归陷入死循环
- 拷贝原型链属性,产生冗余数据
四、节流函数(Throttle)
📌 题目要求
实现节流函数,单位时间内仅执行一次目标函数,过滤高频重复触发,保留this指向和参数传递。
适用场景:滚动加载、窗口resize、按钮频繁点击
🧠 核心原理
利用闭包保存上一次执行时间,每次触发判断时间间隔,达到阈值再执行,否则忽略本次触发。
✅ 代码实现
/**
* 节流函数(时间戳版,立即执行)
* @param {Function} fn - 目标函数
* @param {number} wait - 间隔时间,默认300ms
* @returns {Function} 节流后的函数
*/
function throttle(fn, wait = 300) {
if (typeof fn !== 'function') throw new Error('fn需为函数');
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 测试
const handleScroll = throttle(() => {
console.log('滚动触发', Date.now());
}, 1000);
window.addEventListener('scroll', handleScroll);
❌ 常见易错点
- 未绑定this,导致回调中this指向异常
- 无法接收外部传参,功能受限
- 缺少参数默认值和类型校验
五、防抖函数(Debounce)
📌 题目要求
实现防抖函数,触发后等待指定时间再执行,期间重复触发则重新计时,支持立即执行模式。
适用场景:搜索框联想、输入校验、按钮防重复点击
🧠 核心原理
闭包保存定时器ID,每次触发先清除旧定时器、再新建定时器,确保仅最后一次触发有效。
✅ 代码实现
/**
* 防抖函数
* @param {Function} fn - 目标函数
* @param {number} wait - 等待时间,默认300ms
* @param {boolean} immediate - 是否立即执行,默认false
* @returns {Function} 防抖后的函数
*/
function debounce(fn, wait = 300, immediate = false) {
if (typeof fn !== 'function') throw new Error('fn需为函数');
let timer = null;
return function(...args) {
// 立即执行逻辑
if (immediate && !timer) fn.apply(this, args);
// 清除旧定时器,重新计时
clearTimeout(timer);
timer = setTimeout(() => {
if (!immediate) fn.apply(this, args);
timer = null;
}, wait);
};
}
// 测试
const handleSearch = debounce((e) => {
console.log('搜索内容:', e.target.value);
}, 500);
// 实际使用可绑定input事件
// input.addEventListener('input', handleSearch);
六、数组扁平化(递归+栈)
📌 题目要求
将多维数组转为一维数组,实现递归和非递归(栈)两种方案,面试可灵活作答。
方案1:递归实现(简洁易懂)
// 递归版数组扁平化
function flatten(arr) {
if (!Array.isArray(arr)) return [arr];
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? flatten(val) : val);
}, []);
}
方案2:栈实现(非递归,性能更优)
// 栈版数组扁平化
function flatten(arr) {
if (!Array.isArray(arr)) return [arr];
const res = [];
const stack = [...arr];
while (stack.length) {
const cur = stack.pop();
if (Array.isArray(cur)) {
stack.push(...cur);
} else {
res.push(cur);
}
}
return res.reverse();
}
// 测试
const multiArr = [1, [2, [3, 4], 5], 6];
console.log(flatten(multiArr)); // [1,2,3,4,5,6]
拓展:ES6+ 可使用 arr.flat(Infinity) 快速实现任意维度扁平化,面试可作为补充思路。
📝 前端手写代码答题思路
面试手写代码,按这个步骤写,逻辑清晰、不易出错:
- 先做参数校验:判断类型、空值,处理异常边界
- 理清核心逻辑:紧扣题目原理,代码简洁不冗余
- 保留this和参数:用apply/call绑定this,扩展运算符接收参数
- 处理特殊场景:兼容性、循环引用、异常捕获等
- 规范代码格式:命名语义化,关键逻辑加简短注释
🎯 练习题目
题目:手写数组去重(3种方案)
实现函数去除数组重复元素,支持基本类型去重,推荐三种写法:
- 双重循环 + indexOf
- Set 数据结构去重
- reduce 累加去重
示例:输入
[1,2,2,3,3,3]→ 输出[1,2,3]