快速熟悉前端面试常考手撕题目

参加了一些面试,也看了下其他同学的面经,自己重新整理了下一部分手撕题目,其实我后来也发现自己整理题目后思路会更清晰很多,下面是整理的部分题目

1. bind函数

bind函数会返回一个新的函数,并且把该函数内部的this指向调用bind的第一个参数,具体实现如下:

Function.prototype.myBind = function (thisArg, ...args) {
    if (thisArg == null) {
        thisArg = window || global
    }
    const fn = this
    if (typeof fn !== 'function') {
        throw 'wrong called'
    }
    return function (...otherArgs) {
        return fn.apply(thisArg, args.concat(otherArgs))
    }
}

2. call函数

call函数是对函数进行调用,并将函数内部的this指向指定的对象,在实现时,我们需要拿到被调用的函数 fn以及指向的对象 obj,我们可以在 obj中新增一个属性保存 fn,这样就可以通过 obj.fn()的方式默认将 fn函数的this指向这个 obj

Function.prototype.myCall = function (thisArg, ...args) {
    const fn = this
    if (typeof fn !== 'function') {
        throw 'wrong called'
    }
    if (thisArg == null) {
        thisArg = window || global
    }
    thisArg[fn] = fn
    let res = thisArg[fn](...args)
    delete thisArg[fn]
    return res
}

apply函数的实现和call类似,只不过需要注意的是apply函数的第二个参数是数组。

3. new创建对象

首先我们来看一下用了new创建一个对象,会是怎么用的一个步骤

function Foo(){}
const f = new Foo()

如上所示,我们创建一个对象f,它的构造函数是Foo,那么构造的过程如下:

  • 创建一个新的对象 obj = {}
  • 新创建对象的原型对象是构造函数的原型对象 obj.__proto__= Foo.prototype
  • 将构造函数的this指向该新对象,并执行构造函数,得到返回结果 res
  • 如果 res是对象的话,则返回 res对象,否则返回新创建的对象 obj(就是说如果调用构造函数Foo的返回值是一个指定对象的话,就返回指定对象,否则返回创建的对象)
function myNew(con, ...args) {
    let newObj = Object.create(con.prototype)
    let res = con.apply(newObj, args)
    return typeof res === 'object' && res != null ? res : newObj
}

4. instanceof实现

instanceof操作符用于检测构造函数的 prototype属性是否出现在某个实例对象的原型链上。例如数组实例 arr的构造函数 Array的原型对象就在该实例对象的原型链上。
例如

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);
// expected output: true

console.log(auto instanceof Object);
// expected output: true

在上述例子中,由于 auto是由 Car构造函数创建的实例,那么 Car构造函数的 prototype对象会在 auto实例的原型链上。同样 Object的原型对象也在 auto实例的原型链上。
有了这些基础,我们可以自己来实现instanceof的判断过程

  • 如果左侧对象 left.__proto__ === right.prototype即满足了构造函数的 prototype属性出现在实例对象的原型链上,查找结束,返回 true
  • 如果上一步查找中没有返回 true,那么继续从左侧对象的原型链上查找,令 left = left.__proto__,并继续上一步的判断
  • 如果 left == null时,表示未找到,查找结束
function myInstanceof(left, right) {
    if (typeof left !== 'object' || right == null) {
        return false
    }

    left = left.__proto__
    right = right.prototype
    while (true) {
        if (left == null) return false
        if (left === right) return true
        left = left.__proto__
    }
}

5. 防抖函数

防抖是为了对频繁触发的事件进行一个限制,从触发第一次开始计时,在规定时间内没有再触发的话,计时结束后执行事件,如果规定时间内触发了,则重新计时。
其实这个有点像疫情防控,发现小阳人就开始计时14天,14天内如果有新增小阳人则重新计时14天,如果14天内都没有,则解除风险。
防抖函数典型应用:在搜索框进行输入,输入停止后发送请求进行搜索,如果没有防抖,那么每次输入一个字符就会发起一次请求,这样显然极大增加了无效请求。我们可以用防抖函数对发起请求的过程进行包装,指定一个延迟,延迟期内没有新的输入,延迟结束后则发起请求,这样就可以大大降低无效的请求数量。

function debounce(fn, delay = 500) {
    let timer = null
    return function (...args) {
        // 如果定时器已经开启,则清除定时器
        if (timer) clearTimeout(timer)
        // 重新定时
        timer = setTimeout(() => {
            fn.apply(this, args)
            timer = null
        }, delay)
    }
}

6. 节流函数

节流表示为,我在指定时间内,对一件事情只做一次,像极了摆烂的我。
throttle函数实现方式有如下两种:

function throttle(fn, delay = 200) {
    let timer = null
    return function (...args) {
        // 如果定时器存在,说明还未到时,直接返回
        if (timer) return
        // 设置定时任务,到时后执行fn,并重置定时器为null
        timer = setTimeout(() => {
            fn.apply(this, args)
            timer = null
        }, delay)
    }
}
/**
 * 记录一个开始时间,并在返回的函数中记录一个当前时间
 * 如果当前时间 - 开始时间 > 延迟时间,则执行一次任务
 */
function throttle(fn, delay = 200) {
    let timeStart = Date.now()
    return function (...args) {
        let now = Date.now()
        if (now - timeStart > delay) {
            fn.apply(this, args)
            timeStart = Date.now()
        }
    }
}

7. 函数柯里化

函数柯里化的一种应用是延迟执行,例如

sum(1)(2)(3) = 6
sum(1)(2,3) = 6
sum(1,2,3) = 6

要实现这种功能,我们要确定函数的参数个数,比如丄例中 sum参数个数是3,当收集到3个参数时才执行计算,因此我们可以定义一个函数专门用于参数收集,当收集到的参数为指定个数时,调用函数进行计算;当参数没有收集完成时,返回一个函数继续给调用者用于传参(收集参数)

function sum(x, y, z) {
    return x + y + z
}

function curry(fn) {
    const len = fn.length
    const _args = []
    function argsCollector(...args) {
        _args.push(...args)
        if (_args.length === len) {
            // 参数收集完成
            return fn.apply(this, _args)
        } else {
            // 继续收集参数
            return argsCollector
        }
    }
    return argsCollector
}

const curriedSum = curry(sum)
const res = curriedSum(1)(2, 3)
console.log(res)    // 6

8. 数组扁平化

数组扁平化是指对于如下的一个数组,我们需要转成1维数组的形式

const arr = [1, 2, [3, 4], [5], [[6, 7], 8]]
console.log(flat(arr)) // [1,2,3,4,5,6,7,8]

如何实现呢?我们可以观察下,从最外层来看,arr数组有如下元素,其中有两个是基本数据类型,3个是数组

1、2、[3,4]、[5]、[[6,7],8]

函数 flat的任务是将给定的数组转为1维数组,如果已经是1维数组则停止,我们可以遍历第一层,发现后面三个元素是数组,那么只要我们对后面三个元素进行扁平化,合并上前面的两个元素,得到的就是所需要的结果了。
说的有点绕,总结一下:

  • 检查每个元素是否是数组,如果不是数组,则满足要求,添加到返回结果中
  • 如果是数组,调用 flat函数对该数组进行扁平化,并将扁平化后得到的1维数组中的元素添加到返回结果中
function flat(arr) {
  const res = []
  for (let item of arr) {
    if (item instanceof Array) {
      res.push(...flat(item))
    } else {
      res.push(item)
    }
  }
  return res
}

const arr = [1, 2, [3, 4], [5], [[6, 7], 8]]
console.log(flat(arr)) // [1,2,3,4,5,6,7,8]

9. 数组转树形结构

给定一个数组如下,需要转为指定的树形结构,父节点有 children属性,包括了子节点,而子节点有 parent_id表示父节点的 id

var menu_list = [
    {
        id: '1',
        menu_name: '设置',
        menu_url: 'setting',
        parent_id: 0,
    },
    {
        id: '1-1',
        menu_name: '权限设置',
        menu_url: 'setting.permission',
        parent_id: '1',
    },
    {
        id: '1-2',
        menu_name: '菜单设置',
        menu_url: 'setting.menu',
        parent_id: '1',
    },
    {
        id: '2',
        menu_name: '订单',
        menu_url: 'order',
        parent_id: 0,
    },
    {
        id: '2-1',
        menu_name: '报单审核',
        menu_url: 'order.orderreview',
        parent_id: '2',
    },
    {
        id: '2-2',
        menu_name: '退款管理',
        menu_url: 'order.refundmanagement',
        parent_id: '2',
    },
]

转换后的结果为
图片说明
现在我们来思考下如何实现它。

  1. 首先观察下结果,返回的是一个对象,对象中每一项都是根节点(parent_id为0的节点)的子节点,子节点若还有子节点,则增加一个 children属性并设置为空对象。
  2. 我们可以对数组中所有元素进行遍历,将每个元素的 id作为 key,元素本身作为 value保存在一个对象 temp中(其实map也行)。
  3. 接着我们可以遍历这个对象,拿到每个 value值中的 parent_id属性,如果该属性为0,则表示该节点是第一层子节点,将其直接放入返回的对象 res中。
  4. 如果 parent_id属性不为0,则我们可以用这个 parent_id去找到 temp[parent_id]这个父节点,并将其加入到该父节点的 children属性中(若无则新增该属性)
  5. 遍历结束时,即可得到所需要的对象
function arrayToTree(arr) {
    const res = {}
    const temp = {}
    // 首先将键值对保存在temp中
    arr.forEach((item) => {
        temp[item.id] = item
    })
    // 遍历temp对象,构造返回的对象
    for (let id in temp) {
        let pid = temp[id].parent_id
        let node = temp[id]
        if (!pid) {
            // 第一层的父节点
            res[id] = node
        } else {
            // 找到该节点的父节点
            let parent_node = temp[pid]
            if (!parent_node.children) {
                parent_node.children = {}
            }
            parent_node.children[id] = node
        }
    }
    return res
}

10. 对象深拷贝

首先我们回顾下浅拷贝,浅拷贝中,我们将对象中的每个值直接进行复制,基本数据类型直接复制值,引用数据类型则复制内存地址,当修改其中的引用数据类型的值时,其他引用该数据的地方的值也会进行修改。
比如这么一段代码,它在内存中的结构如下图所示,obj在堆中存放,如果我们只进行浅拷贝,实际拷贝的是它在堆中的地址,任何改变都会引起堆中数据的改变。

var a = 1
var obj = {
  name: 'lihua',
  age: 15
}

image.png
深拷贝则不同了,深拷贝相当于将 obj中所有的数据取出来,重新在堆中开辟一块新的区域,用于存放这些数据,对于这块区域数据的改变,只会表现在对应的引用上,而对于之前的 obj是没有影响的。
那么深拷贝是如何实现的呢?
我们思考下,当我们遍历一个对象的所有键值对的时候,如果 value是基本数据类型,那么直接拷贝就好了,如果是引用数据类型,我们就把这个引用数据类型里的值拿出来,并放入一个同样类型的变量中,然后保存到返回对象里,这个过程是不是也是一个递归?
定义递归函数 deepCopy(),它的参数是一个对象,作用是对一个参数对象进行深拷贝,并将结果返回。

  • 如果遍历到基本数据类型,直接添加到 ret
  • 如果遍历到数组,则创建新数组并添加到 ret
  • 如果遍历到对象,则递归调用 deepCopy()并将返回结果添加到 ret
function deepClone(obj) {
    if (typeof obj !== 'object') return obj
    let ret = {}
    for (let key in obj) {
        let cur = obj[key]
        if (typeof cur !== 'object') {
            ret[key] = cur
        } else if (cur instanceof Array) {
            ret[key] = [...cur]
        } else if (cur instanceof Object) {
            ret[key] = deepClone(cur)
        }
    }
    return ret
}

11. 洋葱模型

实现如下功能

function f(next) {
    console.log(1)
    next()
    console.log(2)
}

function h(next) {
    console.log(3)
    next()
    console.log(4)
}

function g(next) {
    console.log(5)
    next()
    console.log(6)
}

const fn = compose(f, h, g)
fn() // 1,3,5,6,4,2

思路1
通过观察,compose函数是对传入的函数进行整合,返回一个整合后的函数,我们先定义 compose函数的大体框架

function compose(...funcs) {
    return function() {}
}

我们发现,在执行返回的这个函数时,先压入调用栈的函数最后出栈,后压入调用栈的函数先出栈,是一个典型的先进后出顺序,那么可以考虑用递归来实现。

function compose(...funcs) {
    return function() {
        const exec = () => {}
        // 立即执行exec
        exec()
    }
}

下面我们思考下,执行这个exec需要什么样的参数呢?
我们第一次调用的是函数数组的第一个函数,第二次调用第二个......,那么我们可以考虑传递一个索引,索引从0开始。
函数 exec拿到索引后,根据索引取出函数,如果这个函数是 null则停止执行,也就是递归的边界了。

function compose(...funcs) {
    return function() {
        const exec = (index) => {
            const fn = funcs[index]
            // base case
            if(!fn) return
        }
        // 立即执行exec
        exec(0)
    }
}

如果函数 fn存在,那么我们就执行该函数即可,需要注意的是,函数有一个参数 next,它也是一个函数,执行这个 next函数,就相当于重新执行了一次 exec,所以递归表达式可以写为

function compose(...funcs) {
    return function() {
        const exec = (index) => {
            const fn = funcs[index]
            // base case
            if(!fn) return
            fn(function (){
                exec(index + 1)
            })
        }
        // 立即执行exec
        exec(0)
    }
}

思路2
简单来看,假设只有两个函数 fh,那么我们要执行的函数就是

const fn = f.bind(null, h.bind(null, () => {}))

h作为参数传递给 f,可以执行 bind返回一个新的函数,而执行 h时,由于 next并没有什么作用,实际上调用 next()时可以认为执行了一段空函数,所以 h函数的 next函数定义为空函数,作为 bind函数的第二个参数传给 h
那么三个函数的执行时,就等价于下面这个式子

const fn = f.bind(null, h.bind(null, g.bind(null, () => {})))
fn()    // 1 3 5 6 4 2

由此,我们可以看出,实际上我们只需要做一个递归来确定函数的参数即可,定义递归函数 composeFn,它的返回值是一个通过 bind确定了函数参数的新函数,只需要把该函数返回,最后进行调用即可。
需要注意的是,我们依然需要一个索引 index来确定从函数数组中获取到第几个函数作为参数。

function compose(...funcs) {
    const composeFn = (index) => {
        const fn = funcs[index]
        // 如果函数为空,表示到最后一层调用,此时返回一个空函数用于执行即可
        if (!fn) return function () {}
        // 通过递归调用,composeFn(index + 1)可以返回fn函数
        // 所需的next函数参数
        return fn.bind(null, composeFn(index + 1))
    }
    return composeFn(0)
}
#前端面试#
全部评论
谢谢大哥分享的干货😘😘😘
1 回复 分享
发布于 2022-07-21 08:14
太感谢大佬了 受益匪浅
点赞 回复 分享
发布于 2022-07-10 23:22
点赞 回复 分享
发布于 2022-07-11 10:51
第一个bind没有考虑构造函数的原型继承问题样
点赞 回复 分享
发布于 2022-08-08 18:51

相关推荐

10-14 19:28
门头沟学院 C++
点赞 评论 收藏
分享
41 389 评论
分享
牛客网
牛客企业服务