前端面试-手写基础代码题

4 阅读7分钟

前端面试:6大高频题详解(洗牌/防抖/深拷贝)

前端面试里,手写代码是考察JavaScript基础、逻辑思维和工程意识的核心环节。本文整理6道高频手写真题,覆盖数组操作、网络请求、性能优化、数据拷贝四大方向,从问题分析、原理讲解、代码实现、避坑指南一步步拆解,适合面试复盘、日常查漏补缺。

💡 前言:手写代码的考察重点

面试官看手写代码,不止看能不能写出来,更看重这三点:

  1. 基础扎实度:闭包、作用域、类型判断、递归等核心语法的运用
  2. 边界思维:空值、异常、特殊类型、兼容性等场景的处理
  3. 代码规范:可读性、鲁棒性、简洁度,不写冗余、易出错的代码

一、数组乱序: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) 快速实现任意维度扁平化,面试可作为补充思路。

📝 前端手写代码答题思路

面试手写代码,按这个步骤写,逻辑清晰、不易出错:

  1. 先做参数校验:判断类型、空值,处理异常边界
  2. 理清核心逻辑:紧扣题目原理,代码简洁不冗余
  3. 保留this和参数:用apply/call绑定this,扩展运算符接收参数
  4. 处理特殊场景:兼容性、循环引用、异常捕获等
  5. 规范代码格式:命名语义化,关键逻辑加简短注释

🎯 练习题目

题目:手写数组去重(3种方案)

实现函数去除数组重复元素,支持基本类型去重,推荐三种写法:

  • 双重循环 + indexOf
  • Set 数据结构去重
  • reduce 累加去重

示例:输入 [1,2,2,3,3,3] → 输出 [1,2,3]