快速熟悉前端面试常考手撕题目
参加了一些面试,也看了下其他同学的面经,自己重新整理了下一部分手撕题目,其实我后来也发现自己整理题目后思路会更清晰很多,下面是整理的部分题目
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)
} #前端面试#
查看28道真题和解析