概述
准备算法有小半年时间了,也做了一些题,接下来会对相关知识点做一下梳理。
前端的算法和后端算法并没什么本质区别,只不过多了一些和js相关的手写题。本期会对常见手写题做一下整理,和js弱关联的不会出现,比如去重或拍平等。
数据结构与算法部分后续会更新。
本部分源码在这里
apply
用一个指定的this调用一个函数,另外的参数以数组形式作为第二个参数
Function.prototype.apply(thisArg, argsArray)
在具体的实现中,可以用this获取当前函数(因为apply是当前函数调用的),并作为一个属性挂载到thisArg,然后调用。
//为了this,这里不能用箭头函数
Function.prototype.myCall = function (ctx, ...args) {
context = ctx ?? globalThis
context.fn = this
return context.fn(args)
}
call
和apply基本一样,除了另外的参数是以参数列表的形式。
Function.prototype.call(thisArg, ...args)
bind
修改函数的this, 并可以以参数列表的形式预置部分参数。
Function.prototype.bind(thisArg, ...args)
返回替换了this的新函数,因此这里要用闭包,将this和预置的参数提前保存起来。
Function.prototype.myBind = function (ctx, ...args) {
const context = ctx ?? globalThis
return (...params)=> {
this.apply(context, args.concat(params))
}
}
debounce
防抖,即事件停止触发后一段时间执行,如果到达截止事件前再次触发会重新计时
返回一个函数,在调用原来函数的地方调用新函数即可。
function debounce(fn, timeout) {
timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(fn, timeout, ...args);
};
}
throttle
节流,指的是一段时间内最多只能触发一次。
实现的关键在于一段时间内如果触发就忽略,其中有两个细节,即
- 当第一次触发时要不要执行
- 最后一次发生在对应时间段内时,要不要延迟到这段时间结束后执行
实现方法有两个
- 当使用时间戳实现时,只要当前时间-初始时间>time就执行,上述第二项无法满足
- 使用定时器触发时,每次触发时都会保存对应调用然后延迟到时间段结束执行,上述第一项无法满足
这里选择对第二项进行改进,即,当第一个调用时立即执行依次。
function throttle(fn, time) {
let timer = null
let isFirst = true
return function (...args) {
if(isFirst){
fn.apply(this,args)
isFirst=false
}
if(timer===null){
timer= setTimeout(()=>{
timer=null
fn.apply(this,args)
},time)
}
}
}
new
这里考察当我们使用new调用时发生了什么事,即
- 创建一个空对象obj
- 修改obj的__proto__为对应构造函数的原型
- 以obj为this执行当前构造函数,并返回res
- 如果res是对象则返回res,否则返回obj
function CNew(ctr,...args) {
const obj = {};//1
Object.setPrototypeOf(obj, ctr.prototype);//2
const result = ctr.apply(obj, args);//3
return typeof result === "object" ? result : obj;//4
}
instanceof
object instanceof constructor
判断构造函数的原型是否存在于object的原型链上,即沿object的原型链查找constructor直到找到或到null
function myInstanceof(obj, constructor) {
let cur = Object.getPrototypeOf(obj)
while (cur) {
if (cur === constructor.prototype) {
return true
}
cur = Object.getPrototypeOf(cur)
}
return false
}
eventEmitter
即实现一个发布订阅模式,实现思路是
- 初始化时,在eventEmitter实例中维护一个map,key是事件名,value是保存回调的数组。
- 订阅时将,添加或修改对应事件名对应的回调数组
- 发布时,递归调用对应事件的回调数组
- 移除时,删除对应key
- 订阅一次使用的事件时可以在回调中加入移除订阅的逻辑
具体参考源码
promise
实现一个promise,起始就是按Promises/A+的规范翻译成代码。
具体参考源码
deepclone
js中虽然只有七种基本类型和一种对象类型,但复制对象类型时,不同实例的克隆方法可能有很大区别,比如正则类型、日期类型乃至dom节点。
JSON.stringify
深克隆最常使用的是JSON序列化然后解析恢复,但会有以下限制。
可能的错误
- 循环引用
- 不支持bigint
序列化中的过程
- 如果有toJSON()方法,就会用来进行序列化
- Boolean, Number, and String被转化为对应原始类型
- undefined, Functions, and Symbol无效,瑞国出现在数组中会转化为null,否则被忽略
- Date对象实现了toJSON()方法,因此会被当作字符串
- Infinity and NaN和null本身被当作null
- 其他对象只会序列化可枚举属性,比如Map的键值对是不可枚举的。
手动实现
在生产中推荐使用lodash.cloneDeep,手写可以参考其实现,详见源码。