面试中经常被问到的10道题js手写题

328 阅读6分钟

一、手写一个new函数

这里啰嗦一下new后发生了什么:

  1. 一般地,new后面几乎是一个约定的、大写开头的、所谓的、构造函数,(es6的class关键字只是它的语法糖),这里称之为Fn; 第一步会先创建一个对象obj。
  2. Fn构造函数内部的this会指向第一步创建的obj,this即obj。
  3. js中由于的每一个函数(除了箭头函数,这也是为什么箭头函数不能被new的原因之一)中都存在一个prototype属性,许多人喜欢叫他‘原型’,我不这么认为,因为他会跟真正的原型__proto__混淆,我一般会叫他‘灵魂’! 随后,obj对象的原型__proto__的指向会被修改至Fn的‘灵魂’prototype上。
  4. 在Fn中如果没有返回其他的对象(比如 return {} 或者 return function(){}),那么就默认的返回第一步所创建的obj对象。

字多不看,删减版:

  1. 创建一个对象obj。
  2. obj成为构造函数Fn内部的this。
  3. obj的原型指向修改至Fn.prototype上。
  4. 返回obj。

所以你只要理解这几句话,就可以不惧面试官问了(doge)

function myNew (fn, ...args) {
    if (!fn.prototype) return new Error('你小子介个样子传?')
    let newObj = {}
    let res = fn.apply(newObj, args)
    Object.setPrototypeOf(newObj, fn.prototype)
    return ['function', 'object'].includes(typeof res) ? res : newObj 
}

二、手写 typeof

typeof总是返回一个字符,其实现借用 Object.prototype.toString 即可,收一洗~

function myTypeOf (val) {
    let type = Object.prototype.toString.call(val)
    return type.slice(8, -1).toLowerCase()
}

三、手写instanceof

“对,没错,instanceof就是用来检测是不是人家的子类,或者说是不是通过new人家出来的实例对象。”

啰嗦一下: 一般地,instanceof的左侧通常是一个对象,右侧通常是一个构造函数(es6中是class名), 以 obj instanceof Fn 为例,instanceof 就是在检查:在obj的一整条原型链(__proto__)上是否存在Fn.prototype所指向的对象。

function myInstanceOf (obj, Fn) {
    if (!Fn.prototype) return false
    let objProto = Object.getPrototypeOf(obj)
    while (objProto) {
        if (objProto === Fn.prototype) return true
        else objProto = Object.getPrototypeOf(objProto)
    }
    return false
}

四、手写一个解析url的方法

“就是将url的查询参数给扒下来组合成一个对象而已呀~”

这里用到了内置的URL构造函数,他可是一个好东西啊!靓仔可以多多把玩它。

function parserUrl (url) {
    try {
        let urlParams = new URL(str).searchParams
        let res = {}
        for (let item of urlParams.entries()) {
            let [k, v] = item
            if (res[k]) res[k].push(v)
            else res[k] = [v]
        }
        return res
    } catch (e) {
        return new Error('你小子乱写url是不是?')
    }
}
let url = 'https://juejin.cn/?a=666&b=iloveyou&c=888&a=520&d=youloveme'
parserUrl(url) // {"a":["666","520"],"b":["iloveyou"],"c":["888"],"d":["youloveme"]}

五、 手写apply、call

被apply、call的函数会被立即执行,并将传入的对象作为函数内部的this指向。

这两兄弟做的事情是一样的,第一个参数大家都是传入改变this的对象(传Null即默认为window),第二个参数开始就有区别了,前者是接受一个数组,里面装着会被当做参数传入的内容,后者是一系列参数。

“噢,我的上帝!瞧这糟糕的,我打赌肯定有靓仔把它们记岔,我确信!”

Function.prototype.myApply = function (thisArg = window, args) {
    let fn = Symbol('fn')
    thisArg[fn] = this
    let res = thisArg[fn](...args)
    delete thisArg[fn]
    return res
}

Function.prototype.myCall = function (thisArg = window, ...args) {
    let fn = Symbol('fn')
    thisArg[fn] = this
    let res = thisArg[fn](...args)
    delete thisArg[fn]
    return res
}

六、手写bind

“为啥现在才说bind?”

“因为我一直确信先有apply、call,后有bind!”(doge)

bind会返回一个新的函数,这个函数会抛弃他原来(当前)的this指向,而是把调用bind时传入的对象绑定到this上。

// myBind方法要做兼容new操作符处理
Function.prototype.myBind = function myCall (thisArg = window, ...args) {
    // 形如 foo.bind(obj), 那么此刻的this就是foo.
    let fn = this
    // 创建一个空函数,等下会设置他的prototype来做一个参照
    function __Prototype () {}
    function Fbind (...args2) {
        return fn.apply(
            // 如果使用了new(即 new Fbind()),那么这里的this就是被new时创建的新对象
            // 由于new的原理,this instanceof __Prototype 必是true
            // 那么就使用被new时创建的新对象作为this
            (
                this instanceof __Prototype ?  this : thisArg
            ),
            args.concat(args2)
        )
    }
    // 设置参照
    __Prototype.prototype = this.prototype
    
    // 这里其实可以这样做:Fbind.prototype = this.prototype,
    // 再用new包一层的原因是方便以后对返回的Fbind做拓展,
    // 可能会涉及到在Fbind.prototype上做新的方法、属性添加,
    // 如果不包一层会会对this.prototype产生污染。
    
    Fbind.prototype = new __Prototype()
    return Fbind
}

七、手写深拷贝

浅拷贝的话,洒洒水啦

核心思路就是递归进行键值对的复制。

// 希望各位靓仔不要乱搞对象噢
let obj =  { a: 1, b: { c: 2, d: 3 }}
obj.b.c = obj
let obj2 = deepClone(obj)
function deepClone(obj) {
    if (typeof obj !== 'object' || obj === null) return obj
    let weakSet = new WeakSet()
    function dc (obj) {
        if (weakSet.has(obj)) throw new Error('你个小子乱搞对象是不是?')
        weakSet.add(obj)
        let keys = Object.getOwnPropertyNames(obj)
        let newObj = {}
        for (let key of keys) {
            newObj[key] = typeof obj[key] === 'object' ? dc(obj[key]) : obj[key]
        }
        return newObj
    }
    return dc(obj)
}

八、手写防抖、节流

这是js闭包的用处之一。

各位靓仔都打过lol吧?

“当你完成了一次很帅气的单杀,在敌人的尸体面前疯狂点回城进行装X时,只有最后一次且之后不再点回城时,才不会打断真正的回城行为,这就是防抖”

“当你用亚索在兵线上快乐的e来e去时,即便你在疯狂的按e,也不能改变只有在e的冷却时间好了之后才能继续使用e的事实,这就是节流”

// 防抖
function debounce(fn, dealy) {
    let timer = null
    return function (...args) {
        if (timer) {
            clearTimeout(timer)
            timer = null
        }
        timer = setTimeout(fn, dealy, ...args)
    }
}

// 节流
function throttle (fn, delay) {
    let last = Date.now()
    return function (...args) {
        if (Date.now() - last >= delay) {
            fn.apply(this, args)
            last = Date.now()
        }
    }
}

九、手写一个订阅发布模式

由于鄙人及其反感类的写法,所以我将使用原型委托的方式,本质上还是利用了原型链机制。

const EventBus = Object.freeze({
    on (name, cb) {
        if (!arguments.length || typeof cb !== 'function') return
        if (this.eventStack[name]) this.eventStack[name].push(cb)
        else this.eventStack[name] = [cb]
    },
    remove (name, cb) {
        if (!arguments.length || typeof cb !== 'function' || !this.eventStack[name]?.length) return
        this.eventStack[name] = this.eventStack[name].filter(fn => fn !== cb)
    },
    removeMany (nameArr) {
        // 不传参数代表移除所有
        if (!arguments.length) {
            // 由于createEventBus方法内部将eventStack变成了不可写,
            // 所以这里要循环清除,不能直接this.eventStack = {}。
            for (let key of Object.getOwnPropertyNames(this.eventStack)) {
                delete this.eventStack[key]
            }
            return
        }
        if (Array.isArray(nameArr)) {
            for (let item of nameArr) {
                delete this.eventStack[item]
            }
        }
    },
    once (name, cb) {
        if (!arguments.length || typeof cb !== 'function') return
        function _once (...args) {
            Promise.resolve().then(() => cb(...args))
            this.remove(name, _once)
        }
        this.on(name, _once)
    },
    emit (name, ...args) {
        if (!this.eventStack[name]?.length) return
        for (let i = this.eventStack[name].length - 1; i >= 0; i-- ) {
            let fn = this.eventStack[name][i]
            Promise.resolve().then(() => fn?.(...args))
        }
    }
})

function createEventBus () {
    return Object.create(EventBus, {
        eventStack: {
            value: {}
        }
    })
}

let $EventBus = createEventBus()
...
...
...

十、手写一个并发控制的方法

“我一般用这个东西去控制同时发送请求的量或者别的什么东西......”

想象一个场景,100个人(argList)排队上厕所拉屎(handler),这个厕所坑位只有3个(limit),假定坑位有一种魔法,当空闲时会自动将人安排进坑位来拉屎;首先一开始肯定会安排1、2、3号拉进坑位,当然他们的拉屎速度是不一样的,过了一会,2号拉完了,那么他所在的坑位空闲了后马上安排4号进坑拉屎,又过了一会,1号拉完了,所在坑位马上安排5号进坑拉屎,如此往复直到所有人拉完......

const limitHttp = (reqList = [], limitNum = 3) => {
  return new Promise(resolve => {
    if (limitNum > reqList.length) limitNum = reqList.length
    let currentHandelIdx = 0
    let doneCount = 0
    const result = []

    const handle = (params) => {
      result[params.idx] = params
      doneCount += 1
      if (doneCount === reqList.length) {
        resolve(result)
        return false
      }
      if (currentHandelIdx < reqList.length) {
        _fetch(currentHandelIdx++)
      }
    }

    const _fetch = i => {
      reqList[i]().then((res) => {
        handle({
          idx: i,
          status: 1,
          res
        })
      }).catch(err => {
        handle({
          idx: i,
          status: -1,
          res: err
        })
      })
    }

    for (let i = 0 ; i < Math.min(limitNum, reqList.length); i++) {
      currentHandelIdx = i
      _fetch(i)
    }
  })
}