前端面试手写题 —— x% 的理解 + xx% 的摁背,直接秒了!(bushi

236 阅读7分钟

前言

x% 的理解 + xx% 的摁背,直接秒了!

话是这么说,但是还是需要多手操几遍才能 倒反天x

防抖节流

<!DOCTYPE html>
<html lang="en">
<body>
    <button id = "btn">提交</button>
    <script>
        const btn = document.getElementById('btn');
        btn.addEventListener('click',db(handle, 1000))
        function handle(){
            console.log('已经提交');
        }
        // 防抖
        function db(fn, wait){
            let timer = null
            return function(...arg){
                // 销毁上一次的定时器
                clearTimeout(timer)
                timer = setTimeout(() =>{
                    fn.call(this, ...arg)
                }, wait)
            }
        }
        // 节流
        function jl(fn, wait){
            oldtime = Date.now()
            return function(...arg){
                newtime = Date.now()
                if (newtime - oldtime > wait){
                    oldtime = newtime
                    fn.call(this, ...arg)
                }
            }
        }
    </script>
</body>
</html>

拷贝

// 浅拷贝
// 创建一个对象的浅拷贝,只复制对象的直接属性,不递归复制嵌套对象的属性。
let obj = {
    a: 1,
    b: {
      c: 2,
      d: "3"
    }
}

function shallowclone(obj){
    // 创建一个空对象
    let newobj = {}
    // 遍历原对象的直接属性
    for(let key in obj){
        // 确保只复制对象的直接属性,排除原型链上的属性
        if(obj.hasOwnProperty(key))
            newobj[key] = obj[key]
    }
    // 返回新对象
    return newobj
}

// 修改原对象的嵌套属性
obj.b.c = 4
// 输出原对象和浅拷贝后的对象
console.log(obj)                // { a: 1, b: { c: 4, d: "3" } }
console.log(shallowclone(obj))  // { a: 1, b: { c: 4, d: "3" } }

// 深拷贝
// 创建一个对象的深拷贝,递归复制对象的所有嵌套属性,包括数组和函数等复杂对象。
function deepclone(obj){
    // 对null值特殊处理
    if(obj === null) return null
    // 特殊处理Date和RegExp对象
    if(obj instanceof Date) return new Date(obj)
    if(obj instanceof RegExp) return new RegExp(obj)
    // 非对象类型直接返回
    if(typeof obj !== 'object') return obj
    // 创建新对象
    let newobj = new obj.constructor()
    // 遍历所有属性,递归复制嵌套对象
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            newobj[key] = deepclone(obj[key])
        }
    }
    // 返回深拷贝后的对象
    return newobj
}

// 创建深拷贝后的对象
let newobj = deepclone(obj)
// 修改原对象的嵌套属性
obj.b.c = 3
// 输出原对象和深拷贝后的对象,对比差异
console.log(obj)    // { a: 1, b: { c: 3, d: "3" } }
console.log(newobj) // { a: 1, b: { c: 2, d: "3" } }

柯里化

function curry(func){
    // curried用于接收剩余的参数
    return function curried(...args){
        // 如果传入的参数个数大于或等于函数func的参数个数
        if(args.length >= func.length){
            // 则调用函数func,并将剩余的参数传入func
            return func.apply(this, args)
        }else{
            // moreArgs用于接收剩余的参数
            return function(...moreArgs){
                // 调用curried函数,将剩余的参数传入moreArgs
                return curried.apply(this, args.concat(moreArgs))
            }
        }
    }
}


function sum(a, b, c){
    return a + b + c 
}

let curriedSum = curry(sum)
// 打印curriedSum函数的输出
console.log(curriedSum(1)(2)(3))
console.log(curriedSum(1)(2)(3))
console.log(curriedSum(1, 2)(3))
console.log(curriedSum(1)(2, 3))
console.log(curriedSum(1, 2, 3))

数组扁平化

const arr = [1, [2, [3, [4, [5]]]];
console.log(arr.length); // 输出 2

function flatten(arr) {
    let res = [];
    for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            // 如果当前元素是数组,则递归调用 flatten 函数并拼接结果
            res = res.concat(flatten(arr[i]));
        } else {
            // 如果当前元素不是数组,则直接将其加入结果数组
            res.push(arr[i]);
        }
    }
    return res;
}

console.log(flatten(arr)); // 输出 [1, 2, 3, 4, 5]

// 进阶版的 flatten 函数,可以指定扁平化的深度
function flattenV2(arr, deep = 1) {
    let res = [];
    arr.forEach((item) => {
        if (Array.isArray(item) && deep > 0) {
            // 如果当前元素是数组且深度大于 0,则递归调用 flatten 函数并拼接结果
            res = res.concat(flattenV2(item, deep - 1));
        } else {
            // 如果当前元素不是数组或者深度为 0,则直接将其加入结果数组
            res.push(item);
        }
    });
    return res;
}

console.log(flattenV2(arr, 4)); // 输出 [1, 2, 3, 4, 5]

实现 instanceof 方法

function myInstanceof(L, R){
    L = L.__proto__
    R = R.prototype
    while(L !== R){
        if(L === null){
            return false
        }
        L = L.__proto__
    }
    return  true
}

let c = []

console.log(myInstanceof(c, Array)) // true
console.log(myInstanceof({}, Array)) // false
console.log(myInstanceof(c, Object)) // true
console.log(myInstanceof(c, Function)) // false

实现类型判断方法

function getType(value) {
    // 检查值是否为 null,如果是则返回字符串 'null'
    if (value === null) {
        return value + '';
    }
    // 检查值的类型是否为对象
    if (typeof value === 'object') {
        // 使用 Object.prototype.toString 方法获取对象的内部 [[Class]] 属性值,并截取字符串从第 8 位到倒数第 1 位(去除 "[object " 和 "]")
        // 最后将结果转换为小写字母形式
        return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
    } else {
        // 如果值不是 null 且不是对象类型,则直接返回其类型
        return typeof value;
    }
}

// 输出不同值的类型
console.log(getType(null)); // 输出 'null'
console.log(getType([])); // 输出 'array'
console.log(getType(123)); // 输出 'number'
console.log(getType({})); // 输出 'object'
console.log(getType(function(){})); // 输出 'function'

排序(冒泡 + 查找 + 二分)

冒泡

// 冒泡排序算法
function bubbleSort(arr) {
    const n = arr.length;
    for (let i = 0; i < n - 1; i++) {
        for (let j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换 arr[j] 和 arr[j + 1]
                const temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

// 冒泡排序样例
const unsortedArray = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSort(unsortedArray)); // [11, 12, 22, 25, 34, 64, 90]

快速排序

// 快速排序算法函数
function quickSort(arr) {
    // 如果数组长度小于等于1,无需排序,直接返回数组
    if (arr.length <= 1) {
        return arr;
    }
    // 选择数组中的第一个元素作为枢轴(pivot)
    const pivot = arr[0];
    const left = [];  // 用来存放比枢轴小的元素
    const right = [];  // 用来存放比枢轴大的元素
    // 从数组第二个元素开始遍历
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < pivot) {
            // 小于枢轴的元素放入left数组
            left.push(arr[i]);
        } else {
            // 大于等于枢轴的元素放入right数组
            right.push(arr[i]);
        }
    }
    // 递归地对left和right数组进行快速排序,然后将左、枢轴、右三部分连接起来
    return [...quickSort(left), pivot, ...quickSort(right)];
}

// 快速排序样例
const array = [64, 34, 25, 12, 22, 11, 90];
console.log(quickSort(array)); // [11, 12, 22, 25, 34, 64, 90]

二分查找

// 二分查找算法
function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;
    while (left <= right) {
        // 向下取整,即返回小于或等于给定数字的最大整数
        let mid = Math.floor((left + right) / 2);
        if(arr[mid] === target) {
            return mid;
        }
        // 如果找到目标值,直接返回索引
        else if (arr[mid] < target) {
            // 如果中间元素小于目标值,在右半部分继续查找
            left = mid + 1;
        } else {
            // 如果中间元素大于目标值,在左半部分继续查找
            right = mid - 1;
        }
    }
    // 如果未找到目标值,返回 -1
    return -1;
}

// 二分查找样例
const sortedArray = [11, 22, 34, 45, 56, 67, 78, 89];
const target = 34;
const index = binarySearch(sortedArray, target);
if (index !== -1) {
    console.log(`目标值 ${target} 在数组中的索引为:${index}`); // 目标值 34 在数组中的索引为:2
} else {
    console.log(`未找到目标值 ${target}`);
}

this 指向

call

// call 手写  使用剩余参数语法(...args)的目的是将传递给myCall方法的参数列表展开,并作为单独的参数传递给函数
Function.prototype.myCall = function(context, ...args) {
    // 判断是否传入了context,如果没有则默认为全局对象(浏览器环境下是window)
    context = context || window;
    // 为context创建一个唯一的key以避免覆盖已有属性
    const key = Symbol();
    context[key] = this; // 将当前函数设为context的属性
    // 调用函数并传入参数
    const result = context[key](...args);
    // 删除添加的属性
    delete context[key];
    return result;
};

// // 示例演示
function greet(greeting) {
    console.log(`${greeting}, 我的名字是${this.name}`);
}
const person = { name: 'Alice' };
greet.myCall(person, 'Hello'); // 输出:Hello, 我的名字是Alice
greet.call(person, 'Hello'); // 输出:Hello, 我的名字是Alice

apply

// apply手写
Function.prototype.myApply = function(context, argsArray) {
    // 判断是否传入了context,如果没有则默认为全局对象(浏览器环境下是window)
    context = context || window;   
    // 为context创建一个唯一的key以避免覆盖已有属性
    const key = Symbol();
    context[key] = this; // 将当前函数设为context的属性
    let result;
    if (Array.isArray(argsArray)) {
        // 如果argsArray是数组,则使用扩展运算符将数组中的每个元素作为参数传递给函数
        result = context[key](...argsArray);
    } else {
        // 如果argsArray不是数组,则直接调用函数并不传递任何参数
        result = context[key]();
    }  
    // 删除添加的属性
    delete context[key];    
    return result;
};

// 示例演示
function greet(greeting) {
    console.log(`${greeting}, 我的名字是${this.name}`);
}
const person = { name: 'Alice' };
greet.myApply(person, ['Hello']); // 输出:Hello, 我的名字是Alice
greet.apply(person, ['Hello']); // 输出:Hello, 我的名字是Alice

bind

// bind手写
Function.prototype.myBind = function(context, ...args) {
    // 保存当前函数的引用
    const fn = this;  
    // 返回一个新的函数
    return function(...innerArgs) {
        // 在调用时合并传入的参数
        const finalArgs = [...args, ...innerArgs];   
        // 调用原函数并指定this为context,传入合并后的参数
        return fn.apply(context, finalArgs);
    };
};

// 示例使用
function greet(greeting, punctuation) {
    return `${greeting}, 我的名字是${this.name}${punctuation}`;
}
const person = { name: 'Alice' };
// 使用 myBind 方法创建一个绑定了 person 上下文的新函数
const boundFunc = greet.myBind(person, 'Hello');
const boundFunc2 = greet.bind(person, 'Hello');
// 调用绑定后的函数
console.log(boundFunc('!')); // 输出:Hello, 我的名字是Alice!
console.log(boundFunc2('!'));// 输出:Hello, 我的名字是Alice!

数组转树

const city = [
  { id: 12, parent_id: 1, name: "朝阳区" },
  { id: 241, parent_id: 24, name: "田林街道" },
  { id: 31, parent_id: 3, name: "广州市" },
  { id: 13, parent_id: 1, name: "昌平区" },
  { id: 2421, parent_id: 242, name: "上海科技绿洲" },
  { id: 21, parent_id: 2, name: "静安区" },
  { id: 242, parent_id: 24, name: "漕河泾街道" },
  { id: 22, parent_id: 2, name: "黄浦区" },
  { id: 11, parent_id: 1, name: "顺义区" },
  { id: 2, parent_id: 0, name: "上海市" },
  { id: 24, parent_id: 2, name: "徐汇区" },
  { id: 1, parent_id: 0, name: "北京市" },
  { id: 2422, parent_id: 242, name: "漕河泾开发区" },
  { id: 32, parent_id: 3, name: "深圳市" },
  { id: 33, parent_id: 3, name: "东莞市" },
  { id: 3, parent_id: 0, name: "广东省" },
];

function arrayToTree(list) {
  const res = []; // 用于存放树结构的数组
  for (let i = 0; i < list.length; i++) {
    const currentCity = list[i]; // 获取当前城市对象
    const parentId = currentCity.parent_id; // 获取当前城市的父级 ID
    const parent = list.find((item) => item.id === parentId); // 在列表中查找父级城市对象
    if (parent) {
      // 如果找到了父级城市对象
      parent.children = parent.children || []; // 如果父级城市对象不存在 children 属性,则初始化为一个数组
      parent.children.push(currentCity); // 将当前城市对象添加到父级城市对象的 children 数组中
    } else {
      // 如果未找到父级城市对象,说明当前城市是顶级城市
      res.push(currentCity); // 将当前城市对象直接添加到结果数组中
    }
  }
  return res; // 返回组织好的树结构数据
}

console.log(arrayToTree(city)); // 输出转换后的树形结构数据

由于篇幅问题,常考手写题还有 promise 等放在在下期。

参考文章:前端面试必须掌握的手写题:场景篇