【1】前端面试手撕

7 阅读9分钟

1. 手写Object.create()

MDN解释:以一个现有对象作为原型,创建一个新对象(原链接)。

转换一下:把一个对象放在一个新对象的原型链上

MDN示例:

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  },
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // Inherited properties can be overwritten

me.printIntroduction();
// Expected output: "My name is Matthew. Am I human? true"

手写一个Object.create()

// 模拟的create方法,将obj放到新对象的原型链上
Object.prototype.myCreate = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
};

2. 手写instanceof

MDN解释:用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。其返回值是一个布尔值。(原链接)。

转换一下:检测某个构造函数是否存在于某个实例的原型链上

MDN示例:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
const auto = new Car("Honda", "Accord", 1998);

console.log(auto instanceof Car);
// 预期输出:true

console.log(auto instanceof Object);
// 预期输出:true

模拟实现instanceof

function myInstanceof(instance, constructor) {
    // 获取实例的原型
    // Object.getPrototypeOf() 静态方法返回指定对象的原型(即内部 [[Prototype]] 属性的值)。
    let proto = Object.getPrototypeOf(instance);
    // 获取构造函数的原型对象
    let prototype = constructor.prototype;
    while (true) {
        if (!proto) return false;
        if (proto === prototype) return true;
        // 循环原型链
        proto = Object.getPrototypeOf(proto);
    }
}

3. 手写new

new都干了啥事:

  1. 创建一个空对象
  2. 让整个空对象的原型指向构造函数的原型对象,目的是访问构造函数身上的方法和属性
  3. 让构造函数的this指向这个新对象,目的是为了能给这个对象添加新的属性和方法
  4. 返回这个新对象
function myNew(constructor, ...args) {
    // 第一步和第二步
    const newObj = Object.create(constructor.prototype);
    // 第三步并执行这个函数
    const result = constructor.apply(newObj, args);
    // 如果执行了之后返回的是个新对象,就返回新对象,否则返回newObj
    return result && (typeof result === 'object' || typeof result === 'function')
        ? result
        : newObj;
}

4. 防抖

对于用户高频操作,在一定时间内只执行一次,如果在该时间内多次触发,这个时间将会被重置

基础版

function debounce(fn, delay) {
    let timer = null;

    // 核心防抖函数
    const _debounce = function (...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            // 由于这个地方是箭头函数,因此this指向的是外层作用域
            fn.apply(this, args);
        }, delay);
    };
    return _debounce;
}

带取消函数和立即执行函数版

function debounce(fn, delay) {
    let timer = null;
    // 保存上次执行的参数
    let lastArgs = null;

    // 核心防抖函数
    const _debounce = function (...args) {
        lastArgs = args;
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            // 由于这个地方是箭头函数,因此this指向的是外层作用域
            fn.apply(this, args);
        }, delay);
    };

    _debounce.cancel = function () {
        if (timer) clearTimeout(timer);
        timer = null;
    };

    // 调用后将会立即执行一次
    _debounce.flush = function () {
        fn.apply(this, lastArgs);
    };

    return _debounce;
}

5. 节流

在未来的一定时间内,无论触发多少次操作都只执行一次

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

6. 类型判断函数

几种判断类型的函数:

  • typeof:判断基础类型,无法区分null,对象,数组
  • instanceof:判断实例是否属于某个构造函数,在继承的情况下也能判断
  • Object.prototype.toString.call():判断实例的类型,可以区分null和undefined,返回的是字符串[object Type]
function getType(value) {
    if (typeof value === 'object') {
        const typeStr = Object.prototype.toString.call(value);
        return typeStr?.slice(8, -1)?.toLowerCase();
    }
    return typeof value;
}

7. 函数柯里化

将一个多参数函数转化为一系列单参数函数的过程

function curry(fn) {
    return function curried(...args) {
        // 如果参数收集够了就直接执行函数
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        // 否则就返回一个函数,继续收集参数
        return (...nextArgs) => curried(...args, ...nextArgs);
    };
}

// 测试
function sum(a, b, c) {
    return a + b + c;
}

const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2, 3)); // 6

右函数柯里化:函数参数从右往左执行

function rightCurry(fn) {
    return function curried(...args) {
        // 如果参数收集够了就直接执行函数
        if (args.length >= fn.length) {
            return fn.apply(this, args.reverse());
        }
        // 否则就返回一个函数,继续收集参数
        return (...nextArgs) => curried(...nextArgs, ...args);
    };
}

8. 使用setTimeout实现setInterval

为什么不直接使用setInterval(来自AI)

  1. 无法保证时间间隔:实际间隔 = 设定间隔 - 回调执行时间
  2. 任务堆积: 如果回调执行时间超过间隔时间,可能导致任务堆积
  3. 时间累积误差: 如果某次执行时间过长,会导致下次执行被延迟

基础版本

function mySetIntervalSimple(fn, delay) {
    let timer = null;

    function run() {
        fn();
        timer = setTimeout(run, delay);
    }

    // 启动
    timer = setTimeout(run, delay);

    // 返回一个清除函数
    return () => clearTimeout(timer);
}

携带参数版本

function mySetIntervalWithArgs(fn, delay, ...args) {
    let timer = null;

    function run() {
        fn(...args);
        timer = setTimeout(run, delay);
    }

    timer = setTimeout(run, delay);

    return () => clearTimeout(timer);
}

立即执行一次

function mySetIntervalImmediate(fn, delay, immediate = false) {
    let timer = null;

    function run() {
        fn();
        timer = setTimeout(run, delay);
    }

    // 通过timer是否为null来判断是否是第一次执行
    if (immediate && timer === null) {
        fn();
    }
    timer = setTimeout(run, delay);

    return () => clearTimeout(timer);
}

解决时间偏移问题

function mySetInterval(fn, delay) {
    let timer = null;
    let startTime = Date.now();
    // 用于计算下次执行时间
    let count = 0;

    function run() {
        count++;
        fn();

        const nextTime = startTime + count * delay;
        const interval = nextTime - Date.now();

        // 确保延时不为负数
        timer = setTimeout(run, Math.max(0, interval));
    }

    timer = setTimeout(run, delay);

    return () => clearTimeout(timer);
}

9. 数组扁平化

完全扁平化

function flattenArray(arr) {
    return arr.reduce((acc, item) => {
        return Array.isArray(item)
            ? [...acc, ...flattenArray(item)]
            : [...acc, item];
    }, []);
}

指定深度扁平化

function flattenArrayWithDepth(arr, depth) {
    if (!Array.isArray(arr) || depth <= 0) return arr;
    return arr.reduce((acc, item) => {
        return Array.isArray(item)
            ? [...acc, ...flattenArrayWithDepth(item, depth - 1)]
            : [...acc, item];
    }, []);
}

10. 解析url参数

function parseUrlParams(url) {
    const urlObj = new URL(url);
    const params = urlObj.searchParams;
    const result = {};
    params.forEach((value, key) => {
        if (result.hasOwnProperty(key)) {
            // 兼容处理数组的情况
            result[key] = [
                ...(Array.isArray(result[key]) ? result[key] : [result[key]]),
                value,
            ];
        } else {
            result[key] = value;
        }
    });
    return result;
}

11. 循环引用判断

function existCircular(obj, visited = new WeakSet()) {
    if (typeof obj !== 'object' || obj === null) return false;
    if (visited.has(obj)) return true;

    visited.add(obj);
    const values = Object.values(obj);
    for (const value of values) {
        if (existCircular(value, visited)) return true;
    }
    visited.delete(obj);
    return false;
}

骚操作:让编译器帮忙判断(面试可不能这么写,哈哈哈)

function compileCheckCircular(obj) {
    try {
        JSON.stringify(obj);
        return false;
    } catch (error) {
        return error.message.includes('circular');
    }
}

12. 手写sleep函数

function sleep(time) {
    return new Promise((resolve) => setTimeout(resolve, time));
}

async function test() {
    console.log('start', new Date());
    await sleep(1000);
    console.log('end', new Date());
}

13. 手写深比较函数isEqual

function isEqual(obj1, obj2, visited = new WeakMap()) {
    // 处理基本类型和相同引用
    if (obj1 === obj2) {
        return true;
    }

    // 处理NaN的情况
    if (Number.isNaN(obj1) && Number.isNaN(obj2)) {
        return true;
    }

    // 类型检查
    if (typeof obj1 !== typeof obj2) {
        return false;
    }

    if (obj1 === null || obj2 === null) {
        return false; // 到这里说明不全都为null,所以返回false
    }

    if (typeof obj1 !== 'object') {
        return false; // 到这里说明不全都为对象,所以返回false
    }
    // 循环饮用判断
    if (visited.has(obj1)) {
        return visited.get(obj1) === obj2;
    }
    visited.set(obj1, obj2);

    // 进行数组类型的判断
    if (Array.isArray(obj1) !== Array.isArray(obj2)) {
        return false;
    }

    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);

    // 如果两个对象的属性长度不一样则一定不相等
    if (keys1.length !== keys2.length) {
        return false;
    }

    // 如果两个对象的属性长度一样,则比较每个属性的值
    return keys1.every(
        (key) => obj2.hasOwnProperty(key) && isEqual(obj1[key], obj2[key], visited),
    );
}

// 测试所有问题场景
console.log('=== 测试 null ===');
console.log(isEqual(null, null)); // true ✓
console.log(isEqual(null, {})); // false ✓
console.log(isEqual({}, null)); // false ✓

console.log('\n=== 测试数组 vs 对象 ===');
console.log(isEqual([1, 2, 3], { 0: 1, 1: 2, 2: 3 })); // false ✓

console.log('\n=== 测试 NaN ===');
console.log(isEqual(NaN, NaN)); // true ✓
console.log(isEqual(NaN, 0)); // false ✓

console.log('\n=== 测试循环引用 ===');
const circular1 = { a: 1 };
circular1.self = circular1;
const circular2 = { a: 1 };
circular2.self = circular2;
console.log(isEqual(circular1, circular2)); // true ✓

console.log('\n=== 测试嵌套对象 ===');
console.log(isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } })); // true ✓

console.log('\n=== 测试数组 ===');
console.log(isEqual([1, 2, 3], [1, 2, 3])); // true ✓
console.log(isEqual([1, [2, 3]], [1, [2, 3]])); // true ✓
console.log(isEqual([1, 2], [1, 2, 3])); // false ✓

14. 手写字符串repeat函数

function repeat(str, n) {
    return Array(n).fill(str).join('');
}

15. 异步加载图片

function loadImage(src) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.src = src;
        img.onload = () => resolve(img);
        img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
    });
}

16. 对象键名小驼峰和下划线命名方式互转

/**
 * 将键名转换为驼峰命名
 * @param {string} key 键名
 * @returns {string} 驼峰命名
 */
function keyToCamel(key) {
    return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}

/**
 * 将键名转换为下划线命名
 * @param {string} key 键名
 * @returns {string} 下划线命名
 */
function keyToSnake(key) {
    return key.replace(/([A-Z])/g, '_$1').toLowerCase();
}

/**
 * 将对象的键名转换为驼峰命名
 * @param {object} obj 对象
 * @param {function} transformFn 转换函数
 * @returns {object} 转换后的对象
 */
function keyTransform(obj, transformFn) {
    // 先进行是否为对象类型的判断
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }

    // 对于数组内的元素进行递归处理
    if (Array.isArray(obj)) {
        return obj.map(keyTransform);
    }

    const result = {};
    Object.keys(obj).forEach((key) => {
        const camelKey = transformFn(key);
        result[camelKey] = keyTransform(obj[key]);
    });
    return result;
}

17. 数组交并差集

/**
 * 求两个数组的交集:两个数组中都有的元素
 * @param {Array} arr1 数组1
 * @param {Array} arr2 数组2
 * @returns {Array} 交集
 */
function intersection(arr1, arr2) {
    return arr1.filter((item) => arr2.includes(item));
}

/**
 * 求两个数组的并集:两个数组中所有的元素
 * @param {Array} arr1 数组1
 * @param {Array} arr2 数组2
 * @returns {Array} 并集
 */
function union(arr1, arr2) {
    return [...new Set([...arr1, ...arr2])];
}

/**
 * 求两个数组的差集:存在于arr1中,但不存在于arr2中的元素
 * @param {Array} arr1 数组1
 * @param {Array} arr2 数组2
 * @returns {Array} 差集
 */
function difference(arr1, arr2) {
    return arr1.filter((item) => !arr2.includes(item));
}

18. 树形结构与数组的互相转化

示例数据:

// 数据结构:复杂的树形结构示例
const tree = [
    {
        id: 1,
        text: '根节点1',
        parentId: 0,
        level: 1,
        type: 'folder',
        children: [
            {
                id: 2,
                text: '子节点1-1',
                parentId: 1,
                level: 2,
                type: 'folder',
                children: [
                    {
                        id: 5,
                        text: '孙节点1-1-1',
                        parentId: 2,
                        level: 3,
                        type: 'file',
                    },
                    {
                        id: 6,
                        text: '孙节点1-1-2',
                        parentId: 2,
                        level: 3,
                        type: 'file',
                    },
                ],
            },
            {
                id: 3,
                text: '子节点1-2',
                parentId: 1,
                level: 2,
                type: 'folder',
                children: [
                    {
                        id: 7,
                        text: '孙节点1-2-1',
                        parentId: 3,
                        level: 3,
                        type: 'folder',
                        children: [
                            {
                                id: 10,
                                text: '曾孙节点1-2-1-1',
                                parentId: 7,
                                level: 4,
                                type: 'file',
                            },
                        ],
                    },
                ],
            },
            {
                id: 4,
                text: '子节点1-3',
                parentId: 1,
                level: 2,
                type: 'file',
            },
        ],
    },
    {
        id: 8,
        text: '根节点2',
        parentId: 0,
        level: 1,
        type: 'folder',
        children: [
            {
                id: 9,
                text: '子节点2-1',
                parentId: 8,
                level: 2,
                type: 'file',
            },
            {
                id: 11,
                text: '子节点2-2',
                parentId: 8,
                level: 2,
                type: 'folder',
                children: [
                    {
                        id: 12,
                        text: '孙节点2-2-1',
                        parentId: 11,
                        level: 3,
                        type: 'file',
                    },
                ],
            },
        ],
    },
    {
        id: 13,
        text: '根节点3',
        parentId: 0,
        level: 1,
        type: 'file',
    },
];

// 扁平数组结构(对应上面的树形结构)
const objArr = [
    // 根节点
    { id: 1, text: '根节点1', parentId: 0, level: 1, type: 'folder' },
    { id: 8, text: '根节点2', parentId: 0, level: 1, type: 'folder' },
    { id: 13, text: '根节点3', parentId: 0, level: 1, type: 'file' },

    // 根节点1的子节点
    { id: 2, text: '子节点1-1', parentId: 1, level: 2, type: 'folder' },
    { id: 3, text: '子节点1-2', parentId: 1, level: 2, type: 'folder' },
    { id: 4, text: '子节点1-3', parentId: 1, level: 2, type: 'file' },

    // 根节点2的子节点
    { id: 9, text: '子节点2-1', parentId: 8, level: 2, type: 'file' },
    { id: 11, text: '子节点2-2', parentId: 8, level: 2, type: 'folder' },

    // 子节点1-1的孙节点
    { id: 5, text: '孙节点1-1-1', parentId: 2, level: 3, type: 'file' },
    { id: 6, text: '孙节点1-1-2', parentId: 2, level: 3, type: 'file' },

    // 子节点1-2的孙节点
    { id: 7, text: '孙节点1-2-1', parentId: 3, level: 3, type: 'folder' },

    // 子节点2-2的孙节点
    { id: 12, text: '孙节点2-2-1', parentId: 11, level: 3, type: 'file' },

    // 孙节点1-2-1的曾孙节点
    { id: 10, text: '曾孙节点1-2-1-1', parentId: 7, level: 4, type: 'file' },
];

数组转为树形结构

function arrayToTree(arr) {
    const map = {};
    const result = [];

    // 创建映射并初始化 children
    arr.forEach((item) => {
        map[item.id] = { ...item, children: [] };
    });

    // 建立关系
    arr.forEach((item) => {
        if (item.parentId === 0) {
            result.push(map[item.id]);
        } else if (map[item.parentId]) {
            map[item.parentId].children.push(map[item.id]);
        }
    });

    return result;
}

树形结构展开

function treeToArray(tree) {
    const result = [];

    const transform = (tmpTree) => {
        tmpTree.forEach((node) => {
            if (node?.children?.length) {
                transform(node.children);
                delete node.children;
            }
            result.push(node);
        });
    };

    transform(tree);

    return result;
}