前端算法系列(1):常见的手写题

308 阅读4分钟

概述

准备算法有小半年时间了,也做了一些题,接下来会对相关知识点做一下梳理。

image.png 前端的算法和后端算法并没什么本质区别,只不过多了一些和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调用时发生了什么事,即

  1. 创建一个空对象obj
  2. 修改obj的__proto__为对应构造函数的原型
  3. 以obj为this执行当前构造函数,并返回res
  4. 如果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,手写可以参考其实现,详见源码。