【面试必备】前端必备手撕题

4 阅读8分钟

面试之前准备好 hot100 加上以下这些!

实现常见数组实例方法:forEach、map、reduce、filter、entries

Array.prototype.myForEach = function (callback) {
    for (let i = 0; i < this.length; i++) {
        if (i in this) {
            callback(this[i], i, this)
        }
    }
}

Array.prototype.myMap = function (callback) {
    const result = [];
    for (let i = 0; i < this.length; i++) {
        if (i in this) {
            result.push(callback(this[i], i, this));
        }
    }
    return result;
}

Array.prototype.myReduce = function (callback, initialValue) {
    let accumulator = initialValue;
    let startIndex = 0;

    if (accumulator === undefined) {
        while (startIndex < this.length && !(startIndex in this)) {
            startIndex++;
        }
        if (startIndex >= this.length) {
            throw new TypeError("Reduce of empty array with no initial value");
        }
        accumulator = this[startIndex++];
    }

    for (let i = startIndex; i < this.length; i++) {
        if (i in this) {
            accumulator = callback(accumulator, this[i], i, this);
        }
    }
    return accumulator;
}

Array.prototype.myFilter = function (callback) {
    const result = [];
    for (let i = 0; i < this.length; i++) {
        if (i in this && callback(this[i], i, this))  {
            result.push(this[i]);
        }
    }
    return result;
}

// entries 方法一:迭代器(迭代器是生成器的语法糖)
Array.prototype.myEntries1 = function* () {
    for (let i = 0; i < this.length; i++) {
        if (i in this) {
            yield[i, this[i]];
        }
    }
}
// entries 方法二:生成器
Array.prototype.myEntries2 = function () {
    let index = 0;
    const arr = this;
    return {
        next() {
            while (index < arr.length) {
                if (index in arr){
                    return { value: [index, arr[index++]], done: false };
                }
                index++;
            }
            return { done: true };    
        },
        [Symbol.iterator]() {
            return this; // [Symbol.iterator] 使对象成为可迭代对象
        }
    }
}

其他数组手写:reduce 实现 map 和 filter、扁平化、去重、转树

// 用 reduce 实现 map 和 filter
Array.prototype.reduceToMap = function (callback) {
    return this.reduce((acc, cur, index, array) => {
        acc.push(callback(cur, index, array));
        return acc;
    }, [])
}

Array.prototype.reduceToFilter = function (callback) {
    return this.reduce((acc, cur, index, array) => {
        if (callback(cur, index, array)) {
            acc.push(cur);
        }
        return acc;
    }, [])
}

// 数组扁平化
Array.prototype.myFlatten = function(depth = Infinity) {
    const flatten = (arr, currentDepth) => {
        if (currentDepth >= depth) return arr;
        
        const result = [];
        for (const item of arr) {
            if (Array.isArray(item) && currentDepth < depth) {
                result.push(...flatten(item, currentDepth + 1));
            } else {
                result.push(item);
            }
        }
        return result;
    };
    
    return flatten(this, 0);
};

// 数组去重
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr1 = [...new Set(arr)];
const uniqueArr2 = arr.filter((item, index) => arr.indexOf(item) === index);
const uniqueArr3 = arr.reduce((acc, cur) => {
    if (!acc.includes(cur)) {
        acc.push(cur);
    }
    return acc;
}, [])

// 对象数组去重
const objectArr = [
    {id: 1, value: 'a'},
    {id: 1, value: 'a'},
    {id: 2, value: 'b'}
]
const uniqueObjectArr = new Map(objectArr.map(item => [item.id, item])).values();

// 数组转树
function arrToTree(arr) {
    const tree = [];
    const map = new Map();
    arr.forEach(item => map.set(item.id, {...item}));
    for (const item of arr) {
        const { id, parentId } = item;
        if (parentId === null) {
            tree.push(map.get(id));
        } else {
            const parent = map.get(node.parentId);
            if (!parent.children) parent.children = [];
            parent.children.push(node);
        }
    }
    return tree;
}

// 树转数组
function treeToArr(tree) {
    const arr = [];
    const queue = tree.map(node => ({...node, parentId: null}));
    while (queue.length) {
        const {children, ...rest} = queue.shift();
        arr.push(rest);
        if (children?.length) {
            const items = children.map(child => ({...child, parentId: rest.id}))
            queue.push(...items);
        }
    }
    return arr;
}

深拷贝:简易版 & 完全版

// 简易版深拷贝,能拷贝数组和对象
function easeDeepClone (obj) {
    if (typeof obj !== 'object' && obj !== null) {
        return obj;
    }

    const res = Array.isArray(obj) ? [] : {};
    for (const i in obj) {
        if (typeof obj[i] !== 'object' && obj !== null) {
            res[i] = deepClone(obj[i])
        } else {
            res[i] = obj[i];
        }
    }
    return res;
}

// 深拷贝
function deepClone(source, clonedMap = new WeakMap()) {
    // 基本类型,直接原样返回
    if (source === null || typeof source !== 'object') {
        return source;
    }

    // Map 中已有值也直接返回
    if (clonedMap.has(source)) return clonedMap.get(source);

    // 拷贝的是对象和其他复杂类型
    let target;

    if (Array.isArray(source)) {
        target = [];
    } else if (source instanceof Date) {
        target = new Date(source);
    } else if (source instanceof RegExp) {
        target = new RegExp(source.source, source.flags);
    } else if (source instanceof Map) {
        target = new Map();
        clonedMap.set(source, target);
        for (const [key, value] of source) {
            target.set(deepClone(key, clonedMap), deepClone(value, clonedMap));
        }
        return target;
    } else if (source instanceof Set) {
        target = new Set();
        clonedMap.set(source, target);
        for (const value of source) {
            target.add(deepClone(value, clonedMap));
        }
        return target;
    } else {
        target = {};
    }

    clonedMap.set(source, target);

    // 拷贝普通属性
    for (const key in source) {
        if (source.hasOwnProperty(key)) {
            target[key] = deepClone(source[key], clonedMap);
        }
    }

    // 拷贝 Symbol 属性
    const symbolKeys = Object.getOwnPropertySymbols(source);
    for (const symKey of symbolKeys) {
        target[symKey] = deepClone(source[symKey], clonedMap)
    }

    return target;
}

函数实例方法 call、apply、bind & 函数柯里化

// 函数实例方法 call / apply / bind
// call 和 apply 立即调用函数,区别是:call 参数逐个传递,apply 参数以数组形式传递。
// bind() 返回一个绑定了 this 的新函数。
Function.prototype.myCall = function(obj = globalThis, ...args) {
    const fnKey = Symbol();
    obj[fnKey] = this; // 将当前函数(this)赋值给对象的属性
    const result = obj[fnKey](...args);// 此时fnKey作为obj的方法调用,this自然指向obj
    delete obj[fnKey]; // delete 操作符仅用于删除对象属性,无法删除由 var/let/const 定义的变量
    return result;
}

Function.prototype.myApply = function(obj = globalThis, argsArr = []) {
    const fnKey = Symbol();
    obj[fnKey] = this;
    const result = obj[fnKey](...argsArr);
    delete obj[fnKey];
    return result;
}

Function.prototype.myBind = function(obj = globalThis, ...args) {
    const fn = this;
    return function(...newArgs) {
        const fnKey = Symbol();
        obj[fnKey] = fn;
        const result = obj[fnKey](...args, ...newArgs);
        delete obj[fnKey];
        return result;
    }
}

// 函数柯里化(Currying):将一个多参数函数转换为一系列使用一个参数的函数。
// 如果参数没有达到需要的个数,那么返回新的函数;如果达到了就执行函数。
function curry(callback) {
    return function curried(...args) {
        if (args.length >= callback.length) {
            return callback.apply(this, args);
        } else {
            return function(...moreArgs) {
                return curried.apply(this, args.concat(moreArgs))
            }
        }
    }
}

Promise:all、race、any、allSettled、并发池

// Promise.all()
// 当所有 Promise 都成功时,返回所有结果数组(延迟时间不影响输出顺序);
// 如果有一个 Promise 失败,立即拒绝,并返回第一个失败的原因
function myPromiseAll (promises) {
    return new Promise((resolve, reject) => {
        if (promises.length === 0) return resolve([]);

        let completed = 0;
        const res = Array(promises.length);

        promises.forEach((promise, index) => {
            Promise.resolve(promise)
            .then(result => {
                res[index] = result;
                completed++;
                if (completed === promise.length) return resolve(res);
            })
            .catch(error => reject(error)) // 如果有错误,直接reject
        })
    })
}

// Promise.race()
// 返回最先完成( resolve 或 reject )的 Promise 结果
function myPromiseRace(promises) {
    return new Promise((resolve, reject) => {
        if (promises.length) return resolve([]);

        promises.forEach((promise) => {
            Promise.resolve(promise)
            .then(res => resolve(res))
            .catch(error => reject(error))
        })
    })
}

// Promise.any()
// 在任意一个 Promise 被兑现时兑现;仅在所有的 Promise 都被拒绝时才会拒绝
function myPromiseAny (promises) {
    return new Promise((resolve, reject) => {
        if (promises.length === 0) return reject(new AggregateError([], "All promises were rejected"));

        let completed = 0;
        const res = Array(promises.length);

        promises.forEach((promise, index) => {
            Promise.resolve(promise)
            .then(res => resolve(res))
            .catch(error => {
                res[index] = error;
                completed++;
                if (completed === promise.length) {
                    return reject(new AggregateError(res, "All promises were rejected"));
                }
            })
        })
    })
}

// Promise.allSettled()
// 等待所有 Promise 完成(无论成功或失败),返回所有结果
function myPromiseAllSettled(promises) {
    if (promises.length === 0) return resolve([]);

    let completed = 0;
    const res = Array(promises.length);
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
            Promise.resolve(promise)
            .then(result => {
                res[index] = { status: 'fullfilled', value: result };
            })
            .catch(result => {
                res[index] = { status: 'rejected', value: result };
            })
            .finally(() => {
                completed++;
                if (completed === promises.length) return resolve(res);
            })
        })
    })
}

// 限制最大并发数,使异步任务尽快完成并返回结果Promise
function promisePool(capacity, fn, items) {
    const executing = [];
    const remaining = [...items];
    const res = new Array(items.length);

    return new Promise((mainResolve, mainReject) => {
        const run = () => {
            if (remaining.length === 0 && executing.length === 0) {
                mainResolve(res);
                return;
            }

            while (executing.length < capacity && remaining.length > 0) {
                const item = remaining.shift();
                const index = items.indexOf(item);
                const task = fn(item);
                const p = task
                    .then(result => {
                        res[index] = { status: 'fulfilled', value: result };
                        return result;
                    })
                    .catch(error => {
                        res[index] = { status: 'rejected', value: error };
                        throw error;
                    })
                    .finally(() => {
                        executing.splice(executing.indexOf(p), 1);
                        run();
                    })
                executing.push(p);
            }
        }

        run();
    })
}

// 捕获每一个then的错误的方法是使用then的第二个参数,可以return新的Promise或者throw错误
const p1 = new Promise((resolve, reject) => {
    resolve('error');
})

p1.then(res => {
    console.log(res);
    return new Promise((resolve) => {
        resolve('yes');
    })
}, error => {
    console.log(error);
    return new Promise((resolve) => {
        resolve('error');
    })
    // throw new Error ('error');
})

// 在 Promise 链中无论是 throw、Promise.reject()、运行时错误,还是异步操作失败的任何错误都可以被 catch 捕获
// 错误会沿着 Promise 链向后传递并被 catch 或 then 的第二个参数捕获

防抖 & 节流

// 防抖(debounce):通过延迟执行,确保高频事件在指定时间内只执行一次,如窗口大小调整、文字输入搜索。
// 节流(throttle):通过限制执行频率,确保高频事件在指定时间内只执行一次,如防止重复点击,控制滚动懒加载的触发频率。
function debounce(callback, delay) {
    let timer;
    return function(...args) {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            callback.apply(this, args);
        }, delay)
    }
}

function throttle(callback, delay) {
    let lastTime = 0
    return function(...args) {
        let now = new Date();
        if (now - lastTime >= delay) {
            lastTime = now;
            callback.apply(this, args);
        }
    }
}

设计模式:发布订阅、观察者、工厂

// 发布订阅模式
// 维护一个对象,以事件名称为键名,对应的值为回调函数数组
class EventCenter {
    constructor() {
        this.events = {};
    }

    on(event, callback) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(callback);
    }

    emit(event, data) {
        if (!this.events[event]) {
            return;
        }
        this.events[event].forEach(callback => callback(data));
    }

    off(event, callback) {
        if (!this.events[event]) {
            return;
        }
        this.events[event] = this.events[event].filter(item => item !== callback);
    }
}

// 测试
const center = new EventCenter();
const callback1 = data => console.log('Callback 1:', data);
const callback2 = data => console.log('Callback 2:', data);
center.on('news', callback1);
center.on('news', callback2);
center.emit('news', 'Breaking News!'); // Callback 1: Breaking News! Callback 2: Breaking News!
center.off('news', callback1);
center.emit('news', 'Another News'); // Callback 2: Another News


// 观察者模式
// Subject是被观察者(例如图片和其他DOM),维护一个观察者数组
// 1. 维护观察者列表 this.observers
// 2. 提供 addObserver() / removeObserver()
// 3. 状态变化时调用 notifyObservers()
class Subject {
  constructor() {
    this.observers = [];
    this.isIntersecting = false;
  }

  addObserver(observer) {
    this.observers.push(observer)
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(item => item !== observer)
  }
  
  checkIntersection() {
    // ...具体逻辑
    this.isIntersecting = true;
    if (this.isIntersecting) {
        console.log('已加载')
        this.notifyObservers();
    }
  }

  notifyObservers(message) {
    this.observers.forEach(observer => {
      observer.update(message)
    })
  }
}

class Observer {
    constructor() {
        this.name = '懒加载管理器';
    }

    update(message) {
        console.log(`${this.name} ${message}`)
    }
}

const lazyManager = new Observer();
const images = Array.from(document.querySelectorAll('img[data-src]')).map(img => {
  const subject = new Subject(img);
  subject.addObserver(lazyManager);
  return subject;
});
window.addEventListener('scroll', () => {
  lazyManager.checkAll(images);
});
lazyManager.checkAll(images);


// 工厂模式
class Admin { /* ... */ }
class Developer { /* ... */ }

function createUser(role, name) {
  switch(role) {
    case 'admin': return new Admin(name);
    case 'developer': return new Developer(name);
    default: throw new Error('Unknown role');
  }
}

// 使用
const admin = createUser('admin', 'Alice');

懒加载

方法一:现代方法 IntersectionObserver

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>图片懒加载</title>
    <style>
        img {
            width: 300px;
            height: 300px;
            display: block;
            background-color: red;
            margin: 50px auto;
        }
    </style>
</head>
<body>
    <img data-src="1.jpg" alt="图片1">
    <img data-src="2.jpg" alt="图片2">
    <img data-src="3.jpg" alt="图片3">
    <script>
        const images = document.querySelectorAll('img[data-src]');

        const observer = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const image = entry.target;
                    image.src = image.dataset.src; // 使用 dataset 来访问 data-* 开头的自定义属性
                    observer.unobserve(image);
                }
            })
        })

        images.forEach(image => {
            observer.observe(image); // 遍历监听数组元素
        })
    </script>
</body>
</html>

方法二:传统懒加载

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>图片懒加载</title>
    <style>
        img {
            width: 300px;
            height: 300px;
            display: block;
            background-color: red;
            margin: 50px auto;
        }
    </style>
</head>
<body>
    <img data-src="1.jpg" alt="图片1">
    <img data-src="2.jpg" alt="图片2">
    <img data-src="3.jpg" alt="图片3">
    <script>
        const images = document.querySelectorAll('img[data-src]');

        const debounce = (fn, delay = 100) => {
            let timer = null;
            return function() {
                clearTimeout(timer);
                timer = setTimeout(() => fn.apply(this, arguments), delay);
            };
        }

        const lazyLoad = () => {
            images.forEach(image => {
                if (!image.hasAttribute('data-src')) return;
                if (image.getBoundingClientRect().top <= window.innerHeight) {
                    image.src = image.dataset.src;
                    image.removeAttribute('data-src');
                }
            })
        }

        lazyLoad();

        window.addEventListener('scroll', debounce(lazyLoad));
        window.addEventListener('resize', debounce(lazyLoad));
    </script>
</body>
</html>

排序算法

// 快速排序
// 核心思想是分治算法:找一个基准值(pivot),小于基准值的放左边,大于基准值的放右边。
function quickSort(arr, left = 0, right = arr.length - 1) {
    if (left >= right) {
        return arr;
    }
    const pivotIndex = partition(arr, left, right);
    quickSort(arr, left, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, right);
    return arr;
}

function partition(arr, left, right) {
    const pivot = arr[right];
    let i = left;
    for (let j = left; j < right; j++) {
        if (arr[j] < pivot) {
            [arr[i], arr[j]] = [arr[j], arr[i]];
            i++;
        }
    }
    [arr[i], arr[right]] = [arr[right], arr[i]];
    return i;
}

// 冒泡排序
function bubbleSort(arr) {
    for (let i = 0; i < arr.length - 1; i++) {
        let swapped = false;
        for (let j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                swapped = true;
            }
        }
        if (!swapped) {
            break;
        }
    }
    return arr;
}

其他前端常见手写

版本号排序

const version = ['1.1', '1.25', '3', '6.6.6']
function v(versions) {
    const arr = versions.map(item => item.split('.').map(item => parseInt(item)));

    arr.sort((a, b) => {
        const maxLen = Math.max(a.length, b.length);
        for (let i = 0; i < maxLen; i++) {
            const num1 = a[i] || 0;
            const num2 = b[i] || 0;
            if (num1 !== num2) {
                return num1 - num2;
            }
        }
        return 0;
    })
    return arr.map(item => item.join('.'));
}

千分位分割

// 千位数分割
let number = 1234567.89;
// 使用 Number 的 toLocaleString 方法
let formattedNum1 = number.toLocaleString();
// 使用 Intl.NumberFormat 对象
let formater = new Intl.NumberFormat('en-US');
let formattedNum2 = formater.format(number);
// 自定义函数
function formatNumberWithCommas(number) {
    let [integerStr, decimalStr] = number.toString().split('.');
   
    let result = '';
    let count = 0;

    for (let i = integerStr.length - 1; i >= 0; i--) {
        count++;
        if (count % 3 === 0 && i > 0) {
            result = ',' + integerStr[i] + result;
        } else {
            result = integerStr[i] + result;
        }
    }
    if (decimalStr !== undefined) {
        result += '.' + decimalStr;
    }

    return result;
}
let formattedNum3 = formatNumberWithCommas(number);

下划线和驼峰转换

// 下划线转驼峰
const snackToCamel = (name) => {
    return name.replace(/\_(\w)/g, (all, letter) => {
        return letter.toUpperCase();
    })
}

// 驼峰转下划线
const camelToSnack = (name) => {
    return name.replace(/([A-Z])/g, "_$1").toLowerCase()
}

红绿灯

function redLight() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('红灯');
            const res = '红灯'
            resolve(res);
            return;
        }, 3000)
    })
}

function greenLight() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('绿灯');
            resolve();
        }, 1000)
    })
}

function yellowLight() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('黄灯');
            resolve();
        }, 2000)
    })
}

async function run() {
    await redLight();
    await greenLight();
    await yellowLight();
    run();
}

run();