快速熟悉前端面试常考手撕题目
参加了一些面试,也看了下其他同学的面经,自己重新整理了下一部分手撕题目,其实我后来也发现自己整理题目后思路会更清晰很多,下面是整理的部分题目
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', }, ]
转换后的结果为
现在我们来思考下如何实现它。
- 首先观察下结果,返回的是一个对象,对象中每一项都是根节点(
parent_id
为0的节点)的子节点,子节点若还有子节点,则增加一个children
属性并设置为空对象。 - 我们可以对数组中所有元素进行遍历,将每个元素的
id
作为key
,元素本身作为value
保存在一个对象temp
中(其实map也行)。 - 接着我们可以遍历这个对象,拿到每个
value
值中的parent_id
属性,如果该属性为0,则表示该节点是第一层子节点,将其直接放入返回的对象res
中。 - 如果
parent_id
属性不为0,则我们可以用这个parent_id
去找到temp[parent_id]
这个父节点,并将其加入到该父节点的children
属性中(若无则新增该属性) - 遍历结束时,即可得到所需要的对象
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 }
深拷贝则不同了,深拷贝相当于将 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
简单来看,假设只有两个函数 f
和 h
,那么我们要执行的函数就是
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) }#前端面试#