JS源码的手写实现

1,056 阅读6分钟

手写 JS 源码

前言

在面试过程中经常会有手写源码或者是某些 JS 代码的底层原理环节.同时我们也可以学习或者手写一些源码的实现,来加深对代码的理解,从而让我们在项目中能够更加得心应手的去使用.

JS API 的实现

call、apply、bind 的实现

call 和 apply 的实现利用了 this 的指向原理.所以我们可以先了解一下 this 指向的问题,从而实现该方法

this 指向问题

最精简的解释:this 指向调用函数时用的引用

JS 规定,当存在一个对象调用函数时,那么该函数的 this 指向的就是该对象 如果一个函数不存在对象调用它,那么该函数的 this 指向的就是全局的 window

call 实现

基于上面的原理,我们就可以改变函数的 this 指向了

  Object.prototype.myCall=function(context,...args){
    //判断是否为object或者function
    if(typeof context=='object'|| typeof context=='function'){
      context = context||window
    }else{
      //否则,创建一个空的object
      context= Object.create(null)
    }
    //将当前函数挂载到context属性上
    context.__fn__ = this
    //执行该函数,在这里,因为context调用了该函数,所以该函数的this指向的就是传入的context
    const res = context.__fn__(...args)
    //删除挂载的属性
    delete context.__fn__
    return res
  }

apply 实现

apply 和 call 的实现原理是相同的,不同的点在于 apply 接受一个数组作为我们的参数

  Object.prototype.myApply=function(context,args){
    if(typeof context=='object'||typeof context=='function'){
      context = context||window
    }else{
      context = Object.create(null)
    }
    context.__fn__ =this
    const res = context.__fn__(...args)
    delete context.__fn__
    return res
  }

bind 的实现

bind 的和 call 的差别在于它不是一个立即执行的函数,而是返回一个闭包函数

  Object.prototype.myBind = function(context,...args1){
    return (...args2)=>{
      return this.myCall(context,...args1,...args2)
    }
  }

手写 Promise

关于手写 Promise,有个大神介绍的很详细.大家可以参考一下

new 的源码实现

关键字 new 主要是做了三件事情

  • 根据我们的构造函数的原型生成一个临时对象
  • 执行构造函数并传入 this 和相关参数
  • 如果该构造函数返回一个对象我们就返回生成的对象,否则返回我们创建的临时对象
  function myNew(fn,...args){
    const obj = {}
    Object.setPrototypeOf(obj,fn.prototype)
    const res = fn.call(obj,...args)
    return res instanceof Object?res:obj
  }

Array.prototype.reduce 的实现

MDN 上的解释:reduce 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。 reducer 函数:reducer 函数是一个纯函数,通俗来讲就是当你给他提供一个相同的值时,不论执行多少次,它返回的值都是相同的,

reduce 函数接收两个参数,第一个参数就是 reducer、第二个参数就是初始值 reducer 函数接收四个参数,(累计器,当前值,当前索引,源数组)

  Array.prototype.myReduce=function(callback,initVal){
    //有初始值把初始值作为accumulator,否则将数组第一个数作为accumulator
    const accumulator = initVal?initVal||this[0]
    //存储源数组
    let _this = this
    for(let i = initVal?0:1;i<this.length;i++){
      accumulator = callback(accumulator,this[i],i,_this)
    }
    return accumulator
  }

Array.prototype.map

map 函数接受两个参数,第一个参数 callback 是生成新数组元素的函数,第二个参数是执行 callback 时被用作 this

  Array.prototype.myMap=function(callback,thisArg){
    return this.reduce(accumulator,curr,index,arr)=>{
      accumulator.push(callback.call(thisArg,curr,index,arr))
      return accumulator
    },[])
  }

数组扁平化

数组扁平化方案

  let arr=[1,[1,[1,[1,[1]]]]]
  //使用Array.prototype.flat
  arr.flat(4)
  //手动解析

  function flating(arr){
    while (arr.some(item=>Array.isArray(item))) {
      arr = [].concat(...arr)
    }
    return arr
  }

手动实现 Array.prototype.flat

引用 MDN 上对 flat 方法的描述:该方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

  Array.prototype.myFlat = function(num=1){
    if(Array.isArray(this)){

    }else{
      throw this+'.flat is not a function'
    }
  }

手写一个 instanceof

instanceof 能够精确判断对象类型,但是只能用在对象上,但是 instanceof 只支持 Object 类型 所以我们可以根据对象的原型进行判断

  function mInstanceof(cur,target){
    //先解决不能满足条件的情况
    if(typeof cur!=='object'||cur==null)return false

    let p = cur.prototype
    while(true){
      //在原型链上判断
      if(p ==target.prototype)return true
      if(p.prototype==null)return false
      p=Object.getPrototypeOf(p)
    }
  }

手动实现一个原型链继承

我们可以通过一个中间构造函数进行桥接,实现原型的赋值

  function mExtend(child,parent){
    const f=function(){}
    f.prototype = parent.prototype
    child.prototype = new f()
    child.prototype.constructor = child
    child.super = parent.prototype
    return child
  }

封装部分

手写函数聚合

函数的聚合简单来说,就是顺序执行一个函数数组.主要是利用了 reduce 的特性

    const compose = function(fns){
      let len = fns.length
      //先判断传入的数组长度为0或者为1的情况
      if(len ===0)return arg=>arg
      if(len ===1)return fns[0]
      return fns.reduce((a,b)=>(...args)=>b(a(...args)))
    }

手动实现一个深拷贝

深拷贝最最简单的办法可以使用 JSON.parse(JSON.stringify(obj)),但是这种方法肯定不能令人满意. 因为 JS 中不同数据类型存储位置不同,我们在深拷贝中需要关注的就是对象和数组了

//这里利用了WeakMap的key可以存储对象的特性
  function clone(target,map = new WeakMap()){
    if(typeof target=='object'){
      let cloneTarget = Array.isArray(target)?[]:{}
      //处理相互引用的情况
      if(map.has(target)){
        return target
      }
      map.set(target,cloneTarget)

      for(let key in target){
        //递归拷贝
        cloneTarget[key] = clone(target[key],map)
      }
      return cloneTarget
    }else{
      return target
    }
  }

手动实现一个 EventEmitter

EventEmitter 本质是一个发布订阅模式,其中维护了一个对象,对象中的 value 以数组的形式存储了我们注册的事件

class EventEmitter{
  constructor(){
    this.obj={}
  }
  //添加事件
  on(type,callback){
    this.obj[type]=this.obj[type]||[]
    if(this.obj[type].indexOf(callback)==-1){
      this.obj.push(callback)
    }
    return this
  }
  //派发事件
  emit(type,...args){
    let callbackList=this.obj[type]
    callbackList.forEach(item=>item(...args))
  }
  //清除事件
  off(type,callback){
    let callbackList= this.obj[type]
    if(Array.isArray(callbackList)){
      if(callback){
        //如果callback存在,清除指定事件
        const index = callbackList.indexOf(callback)
        if(index!==-1){
          //如果该事件存在,则清除
          callbackList.splice(index,1)
        }
      }else{
        //否则清除所有事件
        callbackList.length=0
      }
    }
    return this
  }
  //注册的事件,执行一次后清除
  once(type,callback){
    const proxy =function(...args){
      callback(...args)
      this.off(type,proxy)
    }
    this.on(type,proxy)
    return this
  }

}

函数柯里化

函数柯里化的定义:把接受多个参数的函数变成一个接受单一参数的函数,当接受的参数数量不满足形参数量时,返回接受剩余参数的函数,否则返回处理结果

其原理简单来说就是利用了闭包函数存储中间参数,最后返回执行的结果

  //基本实现
  function curry(fns,...args1){
    let arr=[]
    return function(...args2){
      arr=arr.concat[...args1,...args2]
      if(arr.length!==fns.length){
        return curry(fns,...arr)
      }else{
        return fns(...arr)
      }
    }
  }
  //最终实现
  let currying = (fns,...args1)=>(...args2)=>(
    (arg)=>fns.length==arg.length?fns(...arg):currying(fns,...arg)
  )([...args1,...args2])

函数防抖

函数防抖可以简单理解为在 n 秒内只能触发一次,如果 n 秒内又触发了事件,那么计时器会被重制重新开始计时

//初次触发会立即执行该函数
  function debounce(fn,wait){
    let timer = null
    return function(...args){
      if(!timer)fn(...args)
      clearTimeout(timer)
      timer = setTimeout(() => {
        timer=null
      }, wait);
    }
  }
  //初次触发等到计时完毕后再触发该函数
  function debounce2(fn,wait){
    let timer = null
    return function(...args){
      if(timer) clearTimeout(timer)
      timer= setTimeout(()=>{
        fn(...args)
      },wait)
    }
  }

函数节流

函数节流可以理解为频繁的触发一个事件时,函数会按照固有的频率去执行,它的主要作用是稀释函数的执行频率. 可以用在屏幕滚动事件或者鼠标滑动事件等地方

  function throttle(fn,wait){
    let canRun =true
    return function(...args){
      if(!canRun) return
      fn(...args)
      canRun=false
      setTimeout(()=>{
        canRun=true
      },wait)
    }
  }