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

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

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
第一个bind没有考虑构造函数的原型继承问题样
点赞 回复 分享
发布于 2022-08-08 18:51
点赞 回复 分享
发布于 2022-07-11 10:51

相关推荐

在之前的时间里一直都非常焦虑,一边秋招已经开始,自己却还没拿到实习,再加上之前有一次很好很接近的机会没有把握住,被打击得信心全无,做了很长的心理建设才开始慢慢投小厂,还好还有些实习机会,分享一下面试八股经验,项目不具参考性,不再列举相关问题深圳某小公司vue  2 和vue3中 v-model区别为什么vue3要使用proxy     比 vue 2使用define property好在哪里?Context加hook是怎么替代Redux实现状态管理的?css盒子模型怪异盒子模型和标准盒子模型计算区别react怎么定义组件bootstrap什么是闭包,你有实际应用过吗?react怎么实现父子组件之间的通信HTML   CSS   js 概念let和const的区别什么是虚拟DOM    什么传统DOM     对比  核心原理diff算法   关键点怎么阻止事件冒泡     event.stopProgapationevent.stopImmediatePropagation    区别其他方法?git是否使用过remote————————————————————————广东钛动基础:HTML中本地存储的方法,区别是什么垂直居中方案在你项目中怎么实现移动端自适应的,什么方案JS基本数据类型怎么判断类型  (回答typeof  instanceof  Object.prototype.toString.call    面试官追问还有吗,ES6中呢)具体判断是否是一个数组用什么方法Array常用方法事件循环机制,有哪些宏任务和微任务说一下闭包,你平常使用过吗,怎么使用的怎么查找对象的键值对怎么知道资源,(比如图片)已经加载完成TS:interface和type区别使用过Pick吗说一下泛型和泛型约束框架(只问了React,大概业务线用的React):React  props和state区别react其他通信方法useState和useEffect这两个hook函数区别想拿到渲染前的数据或函数使用什么hook(没听懂,不会,不知道是不是我理解错了)你常用的hook有哪些useMemo你是怎么使用的,它的作用useCallback使用过吗,原理作用React Router中的组件有哪些Hash路由和history路由,说说原理区别————————————————————————某公司笔试四道选择题  25分两道算法题  75分选择题两道阅读代码选择输出,记不太清,全程开摄像头答题加手机监控,没有记下来大致是第一道考察闭包  第二道考察链表的节点引用和指针操作还有两道分析时间复杂度题两道算法题有一题有点难度,是力扣困难题的变种,没有写过,另一题是二叉树展开为单链表—————————————————————————杭州小公司flex布局 justify-content有哪些属性CSS中keyframes是什么useEffect做什么的uselayoutEffectuseMemo常用git操作git怎么创建新分支GitHub中manage  request干嘛的(可能是想问pull  request)Github你有关注过哪些项目TS泛型作用TS可选属性作用有没有遇到跨域问题,产生原因,是什么的限制  怎么解决本地怎么调样式console    断点使用其他浏览器工具你比较关注返回的哪些信息说一下哪些状态码,分别代表什么同一域名同时发送多个请求,比如100个,但浏览器对这个域名的请求限制只有7个,其他的可能会被挂住,怎么解决CDN原理SEO方法原理ReduxwebSocket给场景,问怎么解决(防抖or节流)防抖节流区别听声音感觉是个年轻清爽帅哥,可惜的是让我开摄像头但面试官没有开——————————————————————————记不太清了闭包原型链机制JS操作Dom方法虚拟Dom和真实DomReact和Vue区别Webpack你有做过哪些配置改动和自定义插件性能相关问题其他一些项目问题一道hot100中的简单算法题——————————————————————————小公司一般总共两到三轮面试一面考察比较基础最后想说的是,不必太过焦虑,我刚开始想的是小公司都不约面,我是不是真的要转行了,是不是自己太菜了,面试表现不好又觉得自己是不是太fw了,有一段时间压力很大很焦虑,躯体化症状严重,触底反弹后摆烂,面试反而多了起来,面试过程也不再担惊受怕,保持一个好心态,在面试中慢慢发现不足,不断优化,不必思虑太多,再怎么样反正去捡垃圾也不会饿死,不要去比较,比较是偷走幸福的小偷。希望大家都一切顺利
点赞 评论 收藏
分享
06-16 00:10
已编辑
蚌埠坦克学院 前端工程师
一面 1.自我介绍。2.说下在实习中做的业务,主要是在做些什么?3.说下小程序的一个大致实现方式,或者说它的设计思路?4.在实现这个组件库的时候,有遇到哪些问题?5.说下React的核心原理?6.为什么要引入虚拟DOM?为什么不直接操作DOM?7.使用虚拟DOM之后就不会触发重排了吗?8.比如说我有一个列表,它有十个元素发生了变化,它其实会产生10个patch,对吗?是应该打1个patch还是10个patch?9.那如果是这样的话,为什么会说他的性能会更好呢?10.fiber是怎么样去实现的?11.fiber他的作用是什么?12.对优先级较高的操作是怎么理解呢?是人为标注哪些优先级高吗?13.它里面实现交替执行的API是什么?14.了解过事件循环或任务队列吗?15.有哪些任务是宏任务,哪些是微任务?16.fetch是宏任务还是微任务?17.说到的计时器,除了使用过setTimeout和setInterval还有使用过哪些定时器?18.比如说现在有一个网页,它打开的速度特别慢,你可能会通过什么方式去优化?19.就第一次请求的时候,他有个一个很复杂算法的前端执行,就是用户他操作的,他可能有一些很重的计算逻辑要在前面去做。很重要的前端逻辑。然后还需要需要在前端里面先给它进行执行,就比如在上传之前有一个计算比较大的一个任务,有什么发放可以对他进行优化?(这块其实没太听明白是怎么一回事?)20.怎么让一个方法变为一个Promise?21.说下链表和数组的区别?22.数组和链表他们随机访问一个元素,数组链表随机访问一个元素的时间复杂度是什么?23.如果一个数组他们已经满了,我还要往里面插入元素要怎么做?24.什么叫原码补码?25.补码用来解决什么问题?26.为什么不直接使用原码?27.了解过array的底层怎么实现的吗?28.了解数组中的哪些方法?29.map怎么实现?30.Array.prototype.sort()怎么实现的?它是稳定的还是不稳定的?为什么不稳定?31.说下HTTP的状态码,然后每个状态码对应的哪个意思?32.除了协商缓存还知道哪些缓存类型?33.知道哪些HTTP的请求头或者响应头?34.跨域的问题怎么解决?35.说下cookie和session的区别?36.什么叫加盐?37.对称加密和非对称加密有什么区别?38.HTTPS他是非对称加密还是对称加密?39.证书是什么东西?他起到一个什么样的作用?40.手写map方法。41.算法:字符串的两数相加。(做太久了)反问二面1.自我介绍。2.上一段实习为什么离职?3.之前做的什么业务?4.聊了小程序相关的一些东西,使用taro和原生小程序开发有什么区别,性能上呢?5.介绍几个react hooks。react中有哪些新的优化?6.介绍下小程序的渲染原理?7.开发的一个babel插件做的在项目中有什么作用?做的是什么工作?8.ES6中新增的一些数组相关的语法?9.ES6新语法?10.对TS泛型的理解?11.type和interface的区别?12.从输入URL到渲染页面的整个过程?13.如果全球各地用户都往这个IP发请求,会不会有什么问题?14.有了解过负载均衡是怎么做的吗?15.服务端怎么去处理前端发送过去的请求的?16.端口是干什么用的?17.什么是CDN?18.怎么去实现一个子元素,针对父元素的一个垂直居中?19.sass是干嘛的?有使用过这些css预处理器吗?20.你觉得前端哪一些方面对你来说是比较有困难?21.讲一下打包构建相关这些知识?22.有了解过行业里面的其他一些构建工具吗?23.package.json中一般包含哪些内容?24.node有接触过吗?他这里的http或者request或者这个express他这个模块是怎么查找的?它的规则是什么样子的?25.esm和cjs的区别?26.算法:组合总和(变题,条件改为`candidates`中的数只能被使用1次)反问
软件开发笔面经
点赞 评论 收藏
分享
评论
41
392
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务