初、中级前端应该要掌握的手写代码实现

33,882 阅读6分钟

过完年马上又要到金三银四面试季了,想必很多同学已经跃跃欲试,提前开始准备面试了,本文就列举了面试过程中一些常见的手写代码实现供参考。或许很多人会问,这些手写代码实现意义何在,社区已经有很多poly-fill或者函数库供选择,何必要自己费力去折腾呢?我的理解是,在真实业务开发场景中,我们真的用不上这些自己写的方法,一个lodash库完全可以满足我们的需求,但此时你仅仅只是一个API Caller ,你经常使用到它,但对它实现原理却一无所知,哪怕它实现起来是非常简单的。所以亲自动手写出它的实现过程,对你理解其中原理是很有帮助的。

模拟call

  • 第一个参数为null或者undefined时,this指向全局对象window,值为原始值的指向该原始值的自动包装对象,如 StringNumberBoolean
  • 为了避免函数名与上下文(context)的属性发生冲突,使用Symbol类型作为唯一值
  • 将函数作为传入的上下文(context)属性执行
  • 函数执行完成后删除该属性
  • 返回执行结果
Function.prototype.myCall = function(context, ...args) {
    context = (context === null || context === undefined) ? window : Object(context)
    const key = Symbol()
    context[key] = this
    const result = context[key](...args)
    delete context[key]
    return result
}

模拟apply

  • 前部分与call一样
  • 第二个参数可以不传,但类型必须为数组或者类数组
Function.prototype.myApply = function(context) {
    context = (context === null || context === undefined) ? window : Object(context)
    const key = Symbol()
    const args = arguments[1]
    context[key] = this
    const result= args ? context[key](...args) : context[key]()
    delete context[key]
    return result
}

模拟bind

  • 使用 call / apply 指定 this
  • 返回一个绑定函数
  • 当返回的绑定函数作为构造函数被new调用,绑定的上下文指向实例对象
  • 设置绑定函数的prototype 为原函数的prototype
Function.prototype.myBind = function(context, ...args) {
    const fn = this
    const bindFn = function (...newFnArgs) {
        return fn.call(
            this instanceof bindFn ? this : context,
            ...args, ...newFnArgs
        )
    }
    bindFn.prototype = Object.create(fn.prototype)
    return bindFn
}

模拟new

  • 创建一个新的空对象
  • this绑定到空对象
  • 使空对象的__proto__指向构造函数的原型(prototype)
  • 执行构造函数,为空对象添加属性
  • 判断构造函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象
const createNew = (Con, ...args) => {
    const obj = {}
    Object.setPrototypeOf(obj, Con.prototype)
    let result = Con.apply(obj, args)
    return result instanceof Object ? result : obj
}

模拟instanceof

  • 遍历左边变量的原型链,直到找到右边变量的 prototype,如果没有找到,返回 false
const myInstanceOf = (left, right) => {
    let leftValue = left.__proto__
    let rightValue = right.prototype
    while(true) {
        if(leftValue === null) return false
        if(leftValue === rightValue) return true
        leftValue = leftValue.__proto__
    }
}

深拷贝

  • 判断类型是否为原始类型,如果是,无需拷贝,直接返回
  • 为避免出现循环引用,拷贝对象时先判断存储空间中是否存在当前对象,如果有就直接返回
  • 开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
  • 对引用类型递归拷贝直到属性为原始类型
const deepClone = (target, cache = new WeakMap()) => {
    if(target === null || typeof target !== 'object') {
        return target
    }
    if(cache.get(target)) {
        return target
    }
    const copy = Array.isArray(target) ? [] : {}
    cache.set(target, copy)
    Object.keys(target).forEach(key => copy[key] = deepClone(target[key], cache))
    return copy
}

函数防抖

函数防抖是在事件被触发n秒后再执行回调,如果在n秒内又被触发,则重新计时。 函数防抖多用于input输入框

  • 箭头函数的this继承自父级上下文,这里指向触发事件的目标元素
  • 事件被触发时,传入event对象
  • 传入leading参数,判断是否可以立即执行回调函数,不必要等到事件停止触发后才开始执行
  • 回调函数可以有返回值,需要返回执行结果
 const debounce = (fn, wait = 300, leading = true) => {
    let timerId, result
    return function(...args) {
        timerId && clearTimeout(timerId)
        if (leading) {
            if (!timerId) result = fn.apply(this, args)
            timerId = setTimeout(() => timerId = null, wait)
        } else {
            timerId = setTimeout(() => result = fn.apply(this, args), wait)
        }
        return result
    }
}

函数节流(定时器)

函数节流是指连续触发事件,但是在 n 秒中只执行一次函数,适合应用于动画相关的场景

const throttle = (fn, wait = 300) => {
    let timerId
    return function(...args) {
        if(!timerId) {
            timerId = setTimeout(() => {
                timerId = null
                return result = fn.apply(this, ...args)
            }, wait)
        }
    }
}

函数节流(时间戳)

const throttle = (fn, wait = 300) => {
    let prev = 0
    let result
    return function(...args) {
        let now = +new Date()
        if(now - prev > wait) {
            prev = now
            return result = fn.apply(this, ...args)
        }
    }
}
函数节流实现方法区别
方法使用时间戳使用定时器
开始触发时立刻执行n秒后执行
停止触发后不再执行事件继续执行一次事件

函数节流(双剑合璧版)

 const throttle = (fn, wait = 300,  {
    // 参数解构赋值
    leading = true,
    trailing = true,
} = {}) => {
    let prev = 0
    let timerId
    const later = function(args) {
        timerId && clearTimeout(timerId)
        timerId = setTimeout(() => {
            timerId = null
            fn.apply(this, args)
      }, wait)
    }
    return function (...args) {
        let now = +new Date()
        if(!leading) return later(args)
        if(now - prev > wait) {
            fn.apply(this, args)
            prev = now
        } else if(trailing) {
            later(args)
        }
    }
 }

leading:false 表示禁用第一次执行

trailing: false 表示禁用停止触发的回调

注意:leading:falsetrailing: false 不能同时设置。

数组去重

const uniqBy = (arr, key) => {
    return [...new Map(arr.map(item => [item[key], item])).values()]
}

const singers = [
    { id: 1, name: 'Leslie Cheung' },
    { id: 1, name: 'Leslie Cheung' },
    { id: 2, name: 'Eason Chan' },
]
console.log(uniqBy(singers, 'id'))

//  [
//    { id: 1, name: 'Leslie Cheung' },
//    { id: 2, name: 'Eason Chan' },
//  ]

原理是利用Map的键不可重复

数组扁平化(技巧版)

利用 toString 把数组变成以逗号分隔的字符串,遍历数组把每一项再变回原来的类型。缺点:数组中元素必须是 Number类型,String类型会被转化成Number

const str = [0, 1, [2, [3, 4]]].toString()
// '0, 1, 2, 3, 4'
const arr = str.split(',')
// ['0','1','2', '3', '4']
const newArr = arr.map(item => +item)
// [0, 1, 2, 3, 4]

const flatten = (arr) => arr.toString().split(',').map(item => +item)

数组扁平化

reduce + 递归

const flatten = (arr, deep = 1) => {
  return arr.reduce((cur, next) => {
    return Array.isArray(next) && deep > 1 ?
      [...cur, ...flatten(next, deep - 1)] :
      [...cur, next]
  },[])
}

const arr = [1, [2], [3, [4]]]
flatten(arr, 1)     // [1, [2], [3, [4]]]
flatten(arr, 2)     // [1,2, [3, 4]]
flatten(arr, 3)     // [1,2, 3, 4]


函数柯里化

const currying = fn =>
    _curry = (...args) => 
        args.length >= fn.length
        ? fn(...args)
        : (...newArgs) => _curry(...args, ...newArgs)

原理是利用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数

发布订阅EventEmitter

class EventEmitter {
    #subs = {}
    emit(event, ...args) {
        if (this.#subs[event] && this.#subs[event].length) {
            this.#subs[event].forEach(cb => cb(...args))
        }
    }
    on(event, cb) {
        (this.#subs[event] || (this.#subs[event] = [])).push(cb)
    }
    off(event, offCb) {
    if (offCb) {
        if (this.#subs[event] && this.#subs[event].length)
            this.#subs[event] = this.#subs[event].filter(cb => cb !== offCb)
      } else {
        this.#subs[event] = []
      }
    }
}

subsEventEmitter私有属性(最新特性参考阮一峰老师的ECMAScript 6 入门),通过on注册事件,off注销事件,emit触发事件

寄生组合继承

  function Super(foo) {
    this.foo = foo
  }
  Super.prototype.printFoo = function() {
    console.log(this.foo)
  }
  function Sub(bar) {
    this.bar = bar
    Super.call(this)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

ES6版继承

  class Super {
    constructor(foo) {
      this.foo = foo
    }
    printFoo() {
      console.log(this.foo)
    }
  }
  class Sub extends Super {
    constructor(foo, bar) {
      super(foo)
      this.bar = bar
    }
  }

ES5的继承,实质是先创造子类的实例对象,然后将再将父类的方法添加到this上。 ES6的继承,先创造父类的实例对象(所以必须先调用super方法,然后再用子类的构造函数修改this

参考

js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]

【进阶 6-2 期】深入高阶函数应用之柯里化

JavaScript专题之跟着 underscore 学节流

如何写出一个惊艳面试官的深拷贝?

头条面试官:你知道如何实现高性能版本的深拷贝嘛?