zoukankan      html  css  js  c++  java
  • JavaScript 手写常用代码

    手写防抖

    防抖,即短时间内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。

    分解需求:

    1. 持续触发不执行
    2. 不触发一段时间再执行

    细节处理:

    1. this的指向
    2. 子函数的参数传递,如event对象
    function debounce(func, wait){
      let timeout
      return function(){
        let context = this
        let args = arguments
        
        clearTimeout(timeout)
        timeout = setTimeout(function(){
          func.apply(this,args)
        },wait)
      }
    }
    

    手写节流

    防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器。目前有两种方式实现节流,一种是使用时间戳另一种是使用定时器

    使用时间戳

    使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

    function throttle(func,awit){
      let context,args
      let previous = 0
      return function(){
        context = this
        args = arguments
        let now = +new Date()
        //判断当前时间-之前时间如果大于时间周期,则执行
        if(now - previous > awit){
          func.apply(context,args)
          previous = now
        }
      }
    }
    

    这种方法是

    1. 事件首次触发就会执行

    2. 事件停止后会立刻停止执行

    使用定时器

    当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

    function throttle(func,wait){
      let context,args,timeout
      return function(){
        context = this
       	args = arguments
        if(!timeout){
          timeout = setTimeout(function(){
            timeout = null
            func.apply(context,args)
          },wait)
        }
      }
    }
    

    这种方法是

    1. 事件首次触发后并不会立即执行
    2. 事件停止后不会立刻停止执行,会等最后一次执行完

    时间戳与定时器的混合

    由于这两种方法会有不一样的效果,我们可以将两者混合一起使用,这样会得到两者共同的特点

    首次触发会执行,并且也会有最后一次执行

    function throttle(func,wait){
      let context,args,timeout
      let previous = 0
      //定时器延迟执行的函数
      let later = function(){
        previous = +new Date()
        timeout = null
        func.apply(context,args)
      }
      let throttled function(){
        context = this
        args = arguments
        let now = +new Date()
        let remaing = wait - (now - previous)
        //判断是否有剩余时间,也就是判断是否是首次触发和是否还有剩余时间
        if(remaing <= 0){
          if(timeout){
            clearTimeout(timeout)
            timeout = null
          }
          func.apply(context,args)
      		previous = +new Date()
        }
        //判断有剩余时间,再判断是否有定时器,如果没有则设置定时器,也就是最后一次执行
        else if(!timeout){
          timeout = setTimeout(later,remaing)
        }
      }
      return throttled
    }
    

    手写callapplybind

    实现call

    先上终版实现代码:

    //在函数对象原型链上增加mycall属性
    Function.prototype.mycall = function(context){
      var context = context || window;    //判断传过来的对象是否为空,为空则指向全局执行上下文
      context.fn = this 									//将调用者赋给 context 的一个属性
      
      var args = []											  //定义一个用来存放传过来参数的类数组对象
      for(let i=1;i<arguments.length;i++){//将类数组对象arguments除第一个外其他放进数组里
        args.push(arguments[i])
      }
      
      var result=context.fn(...args)			//执行调用者函数,并接收返回参数
      
      delete context.fn										//删除调用者的函数
      return result 											//返回结果
    }
    

    call实现了什么

    举个例子:

    var foo = {
        value: 1
    };
    
    function bar() {
        console.log(this.value);
    }
    
    bar.call(foo); // 1
    

    注意两点:

    1. call 改变了 this 的指向,指向到 foo
    2. bar 函数执行了

    模拟实现思路

    那么我们该怎么模拟实现这两个效果呢?

    试想当调用 call 的时候,把 foo 对象改造成如下:

    var foo = {
        value: 1,
        bar: function() {
            console.log(this.value)
        }
    };
    
    foo.bar(); // 1
    

    这个时候 this 就指向了 foo,是不是很简单呢?

    但是这样却给 foo 对象本身添加了一个属性,这可不行呐!

    不过也不用担心,我们用 delete 再删除它不就好了~

    所以我们模拟的步骤可以分为:

    1. 将函数设为对象的属性
    2. 执行该函数
    3. 删除该函数

    以上个例子为例,就是:

    // 第一步
    foo.fn = bar
    // 第二步
    foo.fn()
    // 第三步
    delete foo.fn
    

    fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。

    实现apply

    applycall并没有太多不同,只是在参数方面,call是一个一个传参,而apply是多个参数的数组传参(或者类数组对象)。

    终版代码:

    Function.prototype.myCall = function(context = window, ...args) {
      
      let fn = Symbol("fn");
      context[fn] = this;
    
      let res = context[fn](...args);//重点代码,利用this指向,相当于context.caller(...args)
    
      delete context[fn];
      return res;
    }
    

    实现bind

    最终代码:

    Function.prototype.mybind = function(context){
      if(typeof this !=='function'){
        throw new TypeError('Errror')
      }
      const _this = this;
      const args = [...arguments].slice(1);
      return function F(){
        return  res  = this instanceof F ?  new _this(...args,...arguments) 
        :  _this.apply(context,args.concat(...arguments))
      }
    }
    

    bind实现了什么

    bindcallapply同为更改this指向的方法,但bind同时也需要执行以下的任务:

    1. 改变this指向

    2. 由于需要延迟执行,需要返回一个函数

    3. 参数传入可分两次传入

    4. 当返回的函数作为构造器时,需要使的原有的this失效而让this返回指向实例

    5. 需要返回的函数原型与调用相同

    new的实现

    我们看new都做了什么:

    1. 创建一个新对象,并继承其构造函数的prototype,这一步是为了继承构造函数原型上的属性和方法

    2. 执行构造函数,方法内的this被指定为该新实例,这一步是为了执行构造函数内的赋值操作

    3. 返回新对象(规范规定,如果构造方法返回了一个对象,那么返回该对象,否则返回第一步创建的新对象)

    上代码:

    function objectFactory() {
    		//使用一个新的对象,用于接收原型并返回
        var obj = new Object(),
    		//将第一个参数(也就是构造函数)进行接收
        Constructor = [].shift.call(arguments);
    		//将原型赋给新对象的_proto_
        obj.__proto__ = Constructor.prototype;
    		//利用构造函数继承将父函数的属性借调给子函数
        var ret = Constructor.apply(obj, arguments);
    		//如果构造函数已经返回对象则返回他的对象
      	//如果构造函数未返回对象,则返回我们的新对象
        return ret instanceof Object ? ret : obj;
    };
    

    数组去重

    双重循环

    方法比较繁琐点,但兼容性好点,不失为一种方法。

    const unique = function(arr){
        let newarr = []
        let isrepeat
        for(let i =0;i<arr.length;i++){
            isrepeat=false
            for(let j=0;j<newarr.length;j++){
                if(arr[i] === newarr[j]){
                    isrepeat=true
                    break
                }
            }
            if(!isrepeat) newarr.push(arr[i])
        }
        return newarr
    }
    

    indexOf() + filter()

    基本思路:如果索引不是第一个索引,说明是重复值。

    const unique = function(arr){
        let res
        return res = arr.filter((item,index) => {
            return arr.indexOf(item) === index
        })
        return res 
    }
    

    Map

    得益于Map的数据结构,查询速度极快,所以所消耗时间也极少

    const unique = function(arr){
        const newarr = []
        const map = new Map()
        for(let i =0;i<arr.length;i++){
            if1(!map.get(arr[i])){
                map.set(arr[i],1)
                newarr.push(arr[i])
            }
        }
        return newarr
    }
    

    Set

    甚至可以一行代码实现

    const unique = function(arr){
        return [...new Set(arr)]
    }
    

    扁平化

    对于[1, [1,2], [1,2,3]]这样多层嵌套的数组,我们如何将其扁平化为[1, 1, 2, 1, 2, 3]这样的一维数组呢:

    单纯递归

    对于这种树状结构,最方便的方式就是用递归

    function flatten(arr) {
        let res = []
        for(let i =0;i<arr.length;i++){
            if(Array.isArray(arr[i])){
                res=res.concat(flatten(arr[i]))
            }else{
                res.push(arr[i])
            }
        }
        return res
    }
    

    reduce + 递归

    function flatten(arr) {
        return arr.reduce((prev,next) => {
            return prev.concat(next instanceof Array ? flatten(next) : next)
        },[])
    }
    

    ES6的flat()

    const arr = [1, [1,2], [1,2,3]]
    arr.flat(Infinity)  // [1, 1, 2, 1, 2, 3]
    

    深浅拷贝

    深浅拷发生在JavaScript的引用数据类型中。

    浅拷贝

    创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

    以下几个方法都可以实现浅拷贝

    • concat()
    • slice()
    • Object.assign :const returnedTarget = Object.assign(target, source);
    • ...展开运算符

    以上方法都是实现浅拷贝的方法,他们对于首层元素都会一一复制属性,但是如果是多层引用的话,也只会复制地址,不会复制值。

    以下用concat()做示例

    const originArray = [1,[1,2,3],{a:1}];
    const cloneArray = originArray.concat();
    console.log(cloneArray === originArray); // false
    cloneArray[1].push(4);
    cloneArray[2].a = 2; 
    cloneArray.push(6); 
    console.log(originArray); // [1,[1,2,3,4],{a:2}]
    console.log(cloneArray)  // [1,[1,2,3,4],{a:2},[6]]
    

    深拷贝

    深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

    实现深拷贝的方法有两种:

    1. 利用JSON对象中的parsestringify
    2. 利用递归来实现每一层重新创建对象并赋值

    JSON.stringify/parse方法

    JSON.stringify :是将一个 JavaScript 值转成一个 JSON 字符串。

    JSON.parse :是将一个 JSON 字符串转成一个 JavaScript 值或对象。

    const originArray = [1,2,3,4,5];
    const cloneArray = JSON.parse(JSON.stringify(originArray));
    console.log(cloneArray === originArray); // false
    
    const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
    const cloneObj = JSON.parse(JSON.stringify(originObj));
    console.log(cloneObj === originObj); // false
    
    cloneObj.a = 'aa';
    cloneObj.c = [1,1,1];
    cloneObj.d.dd = 'doubled';
    
    console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
    console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
    

    确实实现深拷贝,但是却也是有着缺陷:

    该方法转换时undefinedfunctionsymbol 会在转换过程中被忽略。

    递归方法

    递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作

    function cloneDeep(source,hash = new WeakMap()){
        if(! typeof source === 'object') return source
      	//如果hash 中存在就直接返回,避免循环引用
        if(hash.has(source)) return hash.get(source)
        const targetObj = Array.isArray(source) ? [] : {}
        //在 hash 存储复制的对象
        hash.set(source,targetObj)
      	//将循环复制对象里的属性
        for(let key in source){
          	//对原型上的属性不进行处理
            if(source.hasOwnProperty(key)){
              //判断 遍历的是不是一个对象
                if(source[key] && typeof source[key] === 'object'){
                    // targetObj[keyt] = Array.isArray(source[key]) ? [] : {}
    								//递归深拷贝
                    targetObj[key] = cloneDeep(source[key],hash)
                }else{
                    targetObj[key] = source[key]
                }
            }
        }
        return targetObj
    }
    

    setTimeout模拟实现setInterval

        //主要使用递归的方式进行模拟
    		let  i =0
        function  newSetTime(func,mine){
          function insed(){
            i++
            func()
            setTimeout(insed,mine)
          }
          setTimeout(insed,mine)
    
        }
        function like(){
          console.log(i)
        }
        newSetTime(like,1000)
    

    判断数据类型

    function getType(obj){
      if(obj === null) return obj;
      return typeof obj == 'object' ? Object.prototype.toString.call(obj).replace('[object ','').replace(']','').toLowerCase():typeof obj;
    }
    

    柯里化

    function curry(fn, args) {
        var length = fn.length;
        var args = args || [];
        return function(){
            newArgs = args.concat(Array.prototype.slice.call(arguments));
            if (newArgs.length < length) {
                return curry.call(this,fn,newArgs);
            }else{
                return fn.apply(this,newArgs);
            }
        }
    }
    
    function multiFn(a, b, c) {
        return a * b * c;
    }
    
    var multi = curry(multiFn);
    
    multi(2)(3)(4);
    multi(2,3,4);
    multi(2)(3,4);
    multi(2,3)(4)
    

    实现多参数的链式调用

    function add() {
      let args = [].slice.call(arguments);
      
      let fn = function(){
       let fn_args = [].slice.call(arguments)
       return add.apply(null,args.concat(fn_args))
      }
      
      fn.toString = function(){
        return args.reduce((a,b)=>a+b)
      }
      
      return fn
    }
    add(1); 			// 1
    add(1)(2);  	// 3
    add(1)(2)(3);// 6
    console.log(add(1)(2, 3)(4)); // 6
    add(1, 2)(3); // 6
    add(1, 2, 3); // 6
    
    

    洗牌算法

    const arr = [1,2,3,4,5,6,7,8,9,10];
    const shuffle = ([...arr]) => {
      let m = arr.length;
      while (m) {
        const i = Math.floor(Math.random() * m--);
        [arr[m], arr[i]] = [arr[i], arr[m]];
      }
      return arr;
    };
    console.log(shuffle(arr))
    // [10, 9, 7, 5, 6, 4, 1, 2, 8, 3]
    

    搬运文章

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

    JavaScript基础心法--深浅拷贝

    带你彻底搞清楚深拷、浅拷贝、循环引用

  • 相关阅读:
    团队绩效打分
    软件对标分析
    目前校园百晓生APP与CSDN软件的对比
    Alpha版
    团队项目第一阶段成果展示
    意见汇总
    团队第一阶段冲刺评价
    冲刺(十)
    【WPF学习】第五十八章 理解逻辑树和可视化树
    【WPF学习】第五十七章 使用代码创建故事板
  • 原文地址:https://www.cnblogs.com/chuncode/p/13489625.html
Copyright © 2011-2022 走看看