最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 几个面试常见手写代码

    正文概述 掘金(幻灵尔依)   2021-01-15   385
    • 手写一个 new 函数
    • 手写bind/call/apply函数
    • 手写防抖和节流函数
    • 手写函数柯里化函数
    • 手写一个Promise
    • 手写一个深拷贝函数
    • 手写一个发布订阅模式

    实现 new 过程:

    要点:

    1. 函数第一个参数是构造函数
    2. 实例的__proto__指向构造函数的原型属性prototype
    3. 函数剩余参数要挂载到一个实例对象上
    4. 构造函数有返回值时,就返回这个返回值
    const createObj = function () {
      let obj = {}
      let Constructor = [].shift.call(arguments) // 1
      obj.__proto__ = Constructor.prototype // 2
      let ret = Constructor.apply(obj, arguments) // 3
      return typeof ret === 'object' ? ret: obj // 4
    }
    
    // 使用
    const Fun = function (name) {
      this.name = name
    }
    Fun.prototype.getName = function() {
      alert(this.name)
    }
    let fun = createObj(Fun, 'gim')
    fun.getName() // gim
    

    值得注意的是,es6的class必须用new调用,否则会报错,如下:

    class Fun {
      constructor(name) {
        this.name = name
      }
      getName() {
        alert(this.name)
      }
    }
    let fun = createObj(Fun, 'gim')
    fun.getName() // Uncaught TypeError: Class constructor Fun cannot be invoked without 'new'
    

    手写 call、apply 及 bind 函数

    共同点:

    1. 第一个参数是要绑定的this
    2. 函数内部的 this 其实是要执行绑定的函数(因为三者都是点调用)

    bind

    这里实现简单版本(new 调用结果不一样),复杂版本建议参考此文

    1. bind函数执行后,要返回一个原函数的拷贝
    2. 给返回函数内部的 fn 绑定传入的 context
    Function.prototype.myBind = function(context, ...args) {
      if (typeof this !== 'function') throw 'caller must be a function'
      const fn = this
      return function() {
        return fn.call(context, ...args, ...arguments)
      }
    }
    

    callapply 函数的实现其实都借助了点调用。利用第一个参数做个中转,调用完之后删除。

    call

    Function.prototype.myCall = function(context = windows, ...args) {
      context._fn = this
      const result = context._fn(...args)
      delete context._fn
      return result
    }
    

    apply

    Function.prototype.myApply = function(context = windows, args) {
      context._fn = this
      const result = context._fn(args)
      delete context._fn
      return result
    }
    

    节流和防抖

    刚开始接触这俩概念的时候傻傻分不清楚。

    浏览器的一些事件,如:resize,scroll,keydown,keyup,keypress,mousemove等。这些事件触发频率太过频繁,绑定在这些事件上的回调函数会不停的被调用。会加重浏览器的负担,导致用户体验非常糟糕。

    节流防抖主要是利用了闭包。

    节流

    节流函数来让函数每隔 n 毫秒触发一次。

    // 节流
    function throttle (f, wait = 200) {
      let last = 0
      return function (...args) { // 以下 内部匿名函数 均是指这个匿名函数
        let now = Date.now()
        if (now - last > wait) {
          last = now
          f.apply(this, args) // 注意此时 f 函数的 this 被绑定成了内部匿名函数的 this,这是很有用的
        }
      }
    }
    // 未节流
    input.onkeyup = funciton () {
      $.ajax(url, this.value)
    }
    // 节流
    input.onkeyup = throttle(function () { // throttle() 返回内部匿名函数,所以 input 被绑定到了内部匿名函数的 this 上
      $.ajax(url, this.value) // 注意这个 this 在执行时被 apply 到了内部匿名函数上的 this ,也就是 input
    })
    

    防抖

    防抖函数让函数在 n 毫秒内只触发最后一次。

    // 防抖
    function debounce (f, wait = 200) {
      let timer = 0
      return function (...args) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          f.apply(this, args)
        }, wait)
      }
    }
    // 未防抖
    input.onkeyup = funciton () {
      $.ajax(url, this.value)
    }
    // 防抖
    input.onkeyup = debounce(function () { // debounce() 返回内部匿名函数,所以 input 被绑定到了内部匿名函数的 this 上
      $.ajax(url, this.value) // 注意这个 this 在执行时被 apply 到了内部匿名函数上的 this ,也就是 input
    })
    

    柯里化函数

    柯里化可以利用函数和不同的参数构成功能更加专一的函数。

    柯里化其实就是利用闭包的技术将函数和参数一次次缓存起来,等到参数凑够了就执行函数。

    function curry(fn, ...rest) {
      const length = fn.length
      return function() {
        const args = [...rest, ...arguments]
        if (args.length < length) {
          return curry.call(this, fn, ...args)
        } else {
          return fn.apply(this, args)
        }
      }
    }
    function add(m, n) {
      return m + n
    }
    const add5 = curry(add, 5)
    

    Promise

    要点:

    1. 三种状态的改变:pendding fulfilled rejected
    2. resolve() reject() 函数的实现
    3. 关键点 then 链式调用的实现
    class MyPromise {
      constructor(fn) {
        this.status = 'pedding'
        this.value = null
        this.resolve = this._resolve.bind(this)
        this.reject = this._reject.bind(this)
        this.resolvedFns = []
        this.rejectedFns = []
        try {
          fn(this.resolve, this.reject)
        } catch (e) {
          this.catch(e)
        }
      }
      _resolve(res) {
        setTimeout(() => {
          this.status = 'fulfilled'
          this.value = res
          this.resolvedFns.forEach(fn => {
            fn(res)
          })
        })
      }
      _reject(res) {
        setTimeout(() => {
          this.status = 'rejected'
          this.value = res
          this.rejectedFns.forEach(fn => {
            fn(res)
          })
        })
      }
      then(resolvedFn, rejecetedFn) {
        return new MyPromise(function(resolve, reject) {
          this.resolveFns.push(function(value) {
            try {
              const res = resolvedFn(value)
              if (res instanceof MyPromise) {
                res.then(resolve, reject)
              } else {
                resolve(res)
              }
            } catch (err) {
              reject(err)
            }
          })
          this.rejectedFns.push(function(value){
            try {
              const res = rejectedFn(value)
              if (res instanceof MyPromise) {
                res.then(resolve, reject)
              } else {
                reject(res)
              }
            } catch (err) {
              reject(err)
            }
          })
        })
      }
      catch(rejectedFn) {
        return this.then(null, rejectedFn)
      }
    }
    

    this.resolvedFnsthis.rejectedFns中存放着 then 函数的参数的处理逻辑,待 Promise 操作有了结果就会执行。

    then函数返回一个Promise实现链式调用。

    其实面试的时候主要靠死记硬背,因为有一次 20 分钟让我写 5 个实现(包括promise),,,谁给你思考的时间。。。

    深拷贝

    乞丐版的

    function deepCopy(obj) {
      //判断是否是简单数据类型,
      if (typeof obj == "object") {
        //复杂数据类型
        var result = obj.constructor == Array ? [] : {};
        for (let i in obj) {
          result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
        }
      } else {
        //简单数据类型 直接 == 赋值
        var result = obj;
      }
      return result;
    }
    

    观察者模式和发布订阅模式

    观察者模式观察者Observer和主体Subject都比较清晰,而发布订阅模式的发布和订阅都由一个调度中心来处理,发布者和订阅者界限模糊。

    观察者模式存在耦合,主体中存储的是观察者实例,而 notify 方法遍历时调用了观察者的 update 方法。而发布订阅模式是完全解耦的,因为调度中心中存的直接就是逻辑处理函数。

    要点:都要实现添加/删除/派发更新三个事件。

    观察者模式

    class Subject {
      constructor() {
        this.observers = []
      }
      add(observer) {
        this.observers.push(observer)
        this.observers = [...new Set(this.observers)]
      }
      notify(...args) {
        this.observers.forEach(observer => observer.update(...args))
      }
      remove(observer) {
        let observers = this.observers
        for (let i = 0, len = observers.length; i < len; i++) {
          if (observers[i] === observer) observers.splice(i, 1)
        }
      }
    }
    
    class Observer {
      update(...args) {
        console.log(...args)
      }
    }
    
    let observer_1 = new Observer() // 创建观察者1
    let observer_2 = new Observer()
    let sub = new Subject() // 创建主体
    sub.add(observer_1) // 添加观察者1
    sub.add(observer_2)
    sub.notify('I changed !')
    

    发布订阅模式

    这里使用了还在提案阶段的 class 的私有属性 #handlers,但是主流浏览器已支持。

    class Event {
      // 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
      #handlers = {}
    
      // 事件添加方法,参数有事件名和事件方法
      addEventListener(type, handler) {
        // 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
        if (!(type in this.#handlers)) {
          this.#handlers[type] = []
        }
        // 将事件存入
        this.#handlers[type].push(handler)
      }
    
      // 触发事件两个参数(事件名,参数)
      dispatchEvent(type, ...params) {
        // 若没有注册该事件则抛出错误
        if (!(type in this.#handlers)) {
          return new Error('未注册该事件')
        }
        // 便利触发
        this.#handlers[type].forEach(handler => {
          handler(...params)
        })
      }
    
      // 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
      removeEventListener(type, handler) {
        // 无效事件抛出
        if (!(type in this.#handlers)) {
          return new Error('无效事件')
        }
        if (!handler) {
          // 直接移除事件
          delete this.#handlers[type]
        } else {
          const idx = this.#handlers[type].findIndex(ele => ele === handler)
          // 抛出异常事件
          if (idx === -1) {
            return new Error('无该绑定事件')
          }
          // 移除事件
          this.#handlers[type].splice(idx, 1)
          if (this.#handlers[type].length === 0) {
            delete this.#handlers[type]
          }
        }
      }
    }
    

    推荐

    最近面试让手写Promise,本来想好好写手写函数这篇文章的,但是写时发现了篇好文章,看完感觉自己没啥可写了的那种...

    传送门


    下载网 » 几个面试常见手写代码

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元