最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手撕JS原生代碼

    正文概述 掘金(式溪)   2020-12-06   524

    最近看到有些朋友去大厂面试时,面试官几乎都要求手撕原生代码 (真的手撕哦,不是仅仅懂原理)。这年头不会手撕原生代码,算法,都不好意思叫自己程序员 (=_=),揾食艰难。我自己有些慌了,所以把我所知的记录下来。

    call

    call是Function的一个原型方法,可以改变传入函数的this的指向。具体实现方法如下:

    Function.prototype.mycall = function (context, ...args) {
        context.fn = this
        context.fn(...args)
        context.fn = null
    }
    

    要点是为传入的上下文添加fn属性,以让它调用。在JS里,谁调用,this就指向谁。context.fn() 里的this是指向context,如果是 context.subcontext.fn(),this是指向subcontext。

    apply

    apply也是类似的方法,只是apply传入的参数要求是数组。如果用...args可以把数组解构,这样的话实现方法与call是基本一样。

    Function.prototype.mycall = function (context, args) {
        context.fn = this
        context.fn(...args)
        context.fn = null
    }
    

    new

    在实现bind之前,先研究new的实现,因为接下来的bind需要了解它的原理。当new 一个函数 (类)时,它做了什么?根据MDN,new的时候会发生以下事情:

    通过上述内容,大概可以知道实现方法。

    function mynew (fn, ...args) {
        obj = {}
        const result = fn.call(obj, ...args)
        obj.__proto__ = fn.prototype
        return result !== undefined ? result : obj
    }
    

    bind

    bind的方法就有点不同。它是返回一个函数,这个函数的this指向传入的上下文,还有保存之前的参数。

    先把上述要求实现吧。既然要保存传入的参数,自然要利用闭包

    Function.prototype.mybind = function (context, ...firstargs) {
        const self = this
        return function (...args) {
            firstargs.concat(args)
            self.call(context, ...firstargs.concat(args))
        }
    }
    

    不过还要考虑bind的一个特性,就是

    上述是MDN的官方描述。举个例:

    const obj = {a: "12",b: "54", z: "hello world"}
    
    function bind_demo (s) {
        console.log (this.z)
        this.s = s
    }
    
    const bind_fn = bind_demo.bind(obj, "added")
    bind_fn()  // hello world
    console.log(obj) // { a: '12', b: '54', z: 'hello world', s: 'added' }
    const new_obj = new bind_fn() // undefined
    console.log(new_obj)  // bind_demo { s: 'added' }
    console.log(new_obj instanceof bind_fn) // true
    

    如果直接调用bind后的函数,结果如我们所想的一样。然而当new了bind_fn后,情况发生变化,console.log (this.z)的结果成了undefined,因为这时的this不是指向obj了,而是new所创建的对象,它是bind_demo的实例。所以应当添加一个判断,判断this是指向new创建的类,还是传入的上下文context。

    Function.prototype.mybind = function (context, ...firstargs) {
        const self = this
        const fn_bind =  function (...args) {
           firstargs.concat(args)
           self.call(this instanceof fn_bind ? this : context, ...firstargs.concat(args))
        }
        fn_bind.prototype = self.prototype
        return fn_bind
    }
    

    别忘了,最后还要考虑继承函数的原型。

    数组拍平

    关于数组拍平,我第一时间想到的是用递归来做:

    Array.prototype.myflatten = function () {
        return recursiveflat (this, [])
    }
    
    function recursiveflat (arr, result) {
        if (!Array.isArray(arr)) {
            throw new Error ("The first parameter is not array.")
        }
    
        arr.forEach (item => {
            if (Array.isArray(item)) {
                recursiveflat (item, result)
            } else {
                result.push (item)
            }
        })
        return result
    }
    

    不过能否不用递归呢? 这样效率会更高。可以利用some验测数组有没有数组元素,如果有,利用apply把传入的数组拍平一层,然后把数组与空数组合并。

    function flat(arr){
         while(arr.some(item => Array.isArray(item))){
            arr = [].concat.apply([],arr); // apply会把传入的数组转换为参数
         }
         return arr;
    }
    var arr = [1,2,[3,4,5,[6,7,8],9],10,[11,12]];
    flat(arr);
    

    深拷贝

    这个真的要用递归才能解决。深拷贝的代码实现不难,难是难在把所有情况考虑。一步一步实现吧。

    先从浅拷贝着手:

    function shallowClone (obj) {
        const cloneObj = {}
        const objKey = Object.keys(obj)
    
        objKey.forEach (item => {
            if (Object.prototype.hasOwnProperty.call(obj, item)) {
                cloneObj[item] = obj[item]
            }
        })
        return cloneObj
    }
    

    深拷贝只是在浅拷贝的基础上加上递归:

    function deepClone (obj) {
        const cloneObj = {}
        const objKey = Object.keys(obj)
    
        objKey.forEach (item => {
            if (Object.prototype.hasOwnProperty.call(obj, item)) {
                if (typeof obj[item] === 'object') {
                    cloneObj[item] = deepClone(obj[item])
                } else {
                    cloneObj[item] = obj[item]
                }
            }
        })
        return cloneObj
    }
    

    不过还是有些问题:

    1. 由于 typeof null是 object,所以还要考虑null的问题
    2. 没有考虑数组

    我这里参考 木易杨 大神的写法,详细可以阅读他的实现:

    【进阶4-3期】面试题之如何实现一个深拷贝

    首先先解决null的问题,先封装一个真正的判断object的方法。

    function isObject(obj) {
    	return typeof obj === 'object' && obj != null;
    }
    

    然后开始真正实现深拷贝:

    function isObject(obj) {
    	return typeof obj === 'object' && obj != null;
    }
    
    function deepClone (obj) {
        if(!isObject (obj)) {
            return obj 
        }
    
        const cloneObj = Array.isArray(obj) ? [] : {}
        
        for (let item in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, item)) {
                if (isObject (obj[item])) { // 数组也包含在内
                    cloneObj[item] = deepClone(obj[item])
                } else {
                    cloneObj[item] = obj[item]
                }
            }
        }
        return cloneObj
    }
    

    柯里化

    了解柯里化前,得要知道函数式编程。

    所谓函数式编程是一种编程思想,简单来说,就是把程序里的函数封装成 纯函数,纯函数就是应用数学的函数概念:函数即映射。

    手撕JS原生代碼

    因此往纯函数传入的参数,返回结果都是相同,好像平时做数学题,往函数代入某个数,出来的结果也是相同的。这样做有什麽好处?

    最直观的结果就是可预测,不会如非函数般,每次调用的结果可能不同,例如栈stack的pop方法,从而使得程序更加健壮,更容易测试。还有就是写出来的程序更接近自然语言,例如:

    operate (substract (3), add (4), multiply (10), 7)
    

    即使不懂程序,很容易推断出这是做一系列运算的程序。

    函数式编程的介绍就到这里,终于可以正式谈柯里化。 柯里化

    引用维基百科的定义:

    可以看一下lodash里柯里化函数的用法:

    const _ = require('lodash')
    
    // 要柯里化的函数
    function getSum (a, b, c) { return a + b + c }
    // 柯里化后的函数
    let curried = _.curry(getSum)
    // 测试
    curried(1, 2, 3)
    curried(1)(2)(3)
    curried(1, 2)(3)
    

    三者结局是一样的,第一个curried直接执行,其馀两个则是缓存参数后再执行。

    我们可以利用闭包实现一个柯里化函数:

    function currying (fn) {
        return function curryfn (...args) {
            if (fn.length > args.length) {
                return function () {
                    return curryfn (...args.concat (Array.from (arguments)))
                }
            }
            return fn (...args)
        }
    }
    

    memorize

    既然纯函数的返回结果是可预测,可以利用缓存,把之前传入参数的返回结果存下来,如果之后传入相同参数,则直接返回结果,不用调用函数。 Vue的computed就是运用这原理的。

    function memorize (fn) {
        const cache = {}
    
        return function (arg) {
            if (cache[arg]) {
                return cache[arg]
            }
    
            cache[arg] = fn (arg)
            return cache[arg]
        }
    }
    

    函数组合

    可以利用纯函数和柯里化的特点,把函数组合。通常组合的函数是从右至左执行。组合的实现如下:

    function flowRight (...args) {
        return function (initValue) {
            return args.reverse().reduce ((acc, fn) => fn (acc), initValue)
        }
    }
    
    const toUpper = s => s.toUpperCase()
    const reverse = arr => arr.reverse()
    const first = arr => arr[0]
    const f = flowRight(toUpper, first, reverse)
    console.log(f(['one', 'two', 'three']))  // THREE
    

    防抖与节流

    这两个概念很容易混淆,因为它们要实现的功能都很相似,都是为了防止用户在一段时间内频繁调用函数,不同之处是防抖是指定某个时间点调用函数,如果在该时间点前再次调用,则取消之前的调用函数。

    节流则是调用函数后,在一定时间内不许再次调用。

    这里参考朱德龙老师在前端高手进阶的写法,他的写法是我见过最全面的,详情可阅读 3 个使用场景助你用好 DOM 事件

    先把防抖实现吧。通常网上的写法是:

    function deBounce (fn, wait = 0) {
      let timeout = null
    
      return function deBounced (...args) {
          if (timeout) {
              clearTimeout (timeout)
              timeout = null
          }
          setTimeout (fn(...args), wait)
      }
    }
    

    这样写也没错,不过没有考虑

    1. 函数之后是否需要回调其他函数
    2. 是否需要手动直接调用
    3. 取消调用函数

    朱德龙老师的代现实现是:

    const debounce = (func, wait = 0) => {
      let timeout = null
      let args
    
      function debounced(...arg) {
        args = arg
        if(timeout) {
          clearTimeout(timeout)
          timeout = null 
        }
    
        // 以Promise的形式返回函数执行结果
        return new Promise((res, rej) => {
          timeout = setTimeout(async () => {
            try {
              const result = await func.apply(this, args)
              res(result)
            } catch(e) {
              rej(e)
            }
          }, wait)
        })
      }
    
      // 允许取消
      function cancel() {
        clearTimeout(timeout)
        timeout = null
      }
    
      // 允许立即执行
      function flush() {
        cancel()
        return func.apply(this, args)
      }
    
      debounced.cancel = cancel
      debounced.flush = flush
    
      return debounced 
    }
    

    面试应该不会考得这么细,不过值得学习一下。

    函数节流主要有两种实现方法:时间戳和定时器。时间戳就是规定时间内只能调用一次,在该时间内再次调用无效。

    时间戳:

    function throttle1 (fn, wait = 0) {
       let lastTime = new Date().getTime()
       return function (...args) {
           if (lastTIme - new Date().getTime() >= wait) {
               lastTime = new Date().getTime()
               fn.apply (fn, args)
           }
       }
    } 
    

    定时器指的是规定时间只能调用一次函数,如果规定时间内再次调用,则把它放在下一个规定时间调用。代码实现如下:

    const throttle = (func, wait = 0) => {
     let timeout = null
     let args
     let firstCallTimestamp
    
     function throttled(...arg) {
       if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
       if (!args) {
         console.log('set args:', arg)
         args = arg
       }
    
       if (timeout) {
         clearTimeout(timeout)
         timeout = null
       }
    
       // 以Promise的形式返回函数执行结果
       return new Promise(async(res, rej) => {
         if (new Date().getTime() - firstCallTimestamp >= wait) {
           try {
             const result = await func.apply(this, args)
             res(result)
           } catch (e) {
             rej(e)
           } finally {
             cancel()
           }
         } else {
           timeout = setTimeout(async () => {
             try {
               const result = await func.apply(this, args)
               res(result)
             } catch (e) {
               rej(e)
             } finally {
               cancel()
             }
           }, firstCallTimestamp + wait - new Date().getTime())  // 计算下一个指定时间
         }
       })
     }
    
     // 允许取消
     function cancel() {
       clearTimeout(timeout)
       args = null
       timeout = null
       firstCallTimestamp = null
     }
    
     // 允许立即执行
     function flush() {
       cancel()
       return func.apply(this, args)
     }
    
     throttled.cancel = cancel
     throttled.flush = flush
    
     return throttled
    }
    

    Promise

    这个我之前写过,放过链接:

    Promise的实现原理


    起源地下载网 » 手撕JS原生代碼

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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