从函数节流与防抖浅析this指向问题、闭包问题

1,871 阅读7分钟

从函数节流与防抖浅析this指向问题、闭包问题、原型链问题

一.案例

<body>
    <button message="好好学习">study</button>
    <button message="天天向上">up</button>
 </body>
 button{
      position: absolute;
  }
let btns = document.querySelectorAll("button")
    btns.forEach(function(item){
      let left = 1
      item.addEventListener("click",function(){
        setInterval(function(){
          item.style.left = left++ + 'px';
        },100)
      })
    })

以上分别是html,css,js代码,当我们点击按钮之后按钮会每隔100ms向右移动一个像素。但是当我们多次点击的时候就会发生特殊的情况,我们会发现按钮越点击越快。我们需要的效果是每点击一次100ms过后移动1px,但是当我们连续点击的时候却会越来越快,原因是当你每1ms去点击一次的时候,点击100次,在点击第100次的时候第一次点击产生的定时器将完成回调函数让按钮向右移动1px,之后之前产生的定时器将会以每1ms的速度完成回调函数,但之前的interval没有消失所以就会越来越快。我们所需要的防抖效果是当第一个定时器结束以后才能开始重新计时。

二.函数的防抖与节流

2.1 函数的防抖需要的条件

  1. 对于在事件触发n秒内再执行回调
  2. 如果在n秒内再触发该事件,我们需要重新计时

2.2 防抖带来的问题

  1. 污染全局
  2. 初次触发事件时,会延迟执行(例如:ajax请求第一次不需要延时,输入验证第一次需要延时)

2.3 解决案例问题采用的防抖策略:

let btns = document.querySelectorAll("button")
    var t = null  
    btns.forEach(function(item){
      let left = 1
      item.addEventListener("click",function(){
        clearInterval(t)
        t = setInterval(function(){
          item.style.left = left++ + 'px';
        },100)
      })
    })

当这样处理后只有当前一个计时器完成后才会开启后一个计时器,如果在前一个计时器未完成时再次想触发后一个计时器就会直接消除前一个计时器

2.4 防抖函数的封装

function debounce(fn,time,trigger){
        var t = null
        var debounced = function(){
          var that = this,
              args = arguments;
          if(t){
            clearTimeout(t)
          }
          if(trigger){
            // trigger为true -> 第一次立即触发
            var exec = !t

            // 不管是不是立即触发 都要延迟因为后面是延迟触发
            t = setTimeout(function(){
              t=null
            },time)   // 1.在1s之内这个t不为null及时是你clearTimeout也不为null!!! 当t不为null exec为false就不会再执行fn
                      // 2.当一秒过后触发了定时器 t又为null了 exec为true则又执行fn  所以说t就相当于一个是否执行fn的开关
                      // 3.当你反复触发的时候 就会频繁执行clearTimeout函数 实现防抖
            if(exec){
              fn.apply(that,args)
            }
          }else{
            // 第一次也触发延迟
            t = setTimeout(function(){
              fn.apply(that,args)
            },time)
          }
        }
        debounced.remove = function(){
          clearTimeout(t)
          t=null
        }
        return debounced
      }

2.5 节流函数与防抖函数的区别

函数防抖:n秒内只要你触发事件就会重新计时也就是说你在n内不停触发事件则该事件永远不会被执行
函数节流:n秒之内只会执行一次该事件,即无论你如何点击,都只在n秒之内执行一次。(输入验证)

2.6 封装节流函数

function throttle(fn,delay){
        var t = null
            begin = new Date().getTime()

        return function(){
          var that = this,
              args = arguments,
              cur = new Date().getTime();
          
          clearTimeout(t)

          if(cur-begin >= delay){  // 如果大于等于延迟事件则之间执行 确保n秒之内必触发一次事件 防止因频繁点击重新计时而不触发事件的情况
            fn.apply(that,args)
            begin = cur
          }else{  // 正常延迟执行
            t = setTimeout(function(){
              fn.apply(that,args)
            },delay)
          }
        }
      }

三.闭包问题

我们可以看到无论是防抖函数还是节流函数中都形成了闭包,那么什么是闭包呢?

3.1 闭包原理

官方解释:一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。 换句话说就是内部函数可以访问到其外部函数的变量就会形成闭包。所以我们看到防抖函数中的变量t,以及节流函数中的变量t与变量begin都可以被其内部函数访问以及使用所以形成了闭包。

3.2 闭包的作用

我们知道每个变量从其声明之后都会被回收,也就是说变量都有其生命周期,那么闭包的作用就是延长变量的生命周期。就像节流函数的begin变量,由于每次执行throttle都会访问外部begin变量,所以begin变量被缓存了或者可以理解为建立了引用关系,所以不会在执行后就直接消失了。

3.3 闭包的应用

function fn(){
      let n =1 
      return function(){
        let m = 1
        return function show(){
          console.log('m',++m)   // m累加   
          console.log('n',++n)  // n累加 
        }
        
      }
    }
    let a = fn()()
    a()
    a()
function Fn(){
      let n = 1
      this.sum = ()=>{
        console.log(++n)
      }
    }
    let a = new Fn()
    a.sum()
    a.sum()

疑问:这里构造函数Fn,n是如何实现累加的?闭包的形成在哪里呢?实际上构造函数Fn可以转化为下述写法

function Fn(){
      let n = 1 
      function sum(){
        console.log(++n)
      }
      return {
        sum:sum
      }
    }

也就是说构造函数可以自动延长变量作用域

四.this指向问题

我们可以看到无论是防抖函数还是节流函数我们在函数中都通过that保留了this指向,那么我们为什么要保留这个this指向呢?因为this指向在不同情况下会改变。this指向在不同的情况下是怎么样的呢,我们来探讨一下。

4.1 全局作用域下的this

var a = 1
console.log(window.a === this.a)  // true
console.log(this.a == self.a)  // true
console.log(this.a == frames.a) // true
console.log(this.a == globalThis.a) // true

在浏览器环境下window,this,self,frames,globalThis是一样的含义 并且globalThis在Web以及Node环境下都是通过的。

4.2 类中的this

class Father{
  constructor(){
    this.age = 44
  }
  swim(){
    console.log(this)  // Son
    console.log('Go swmming!!!')
  }
}

class Son extends Father {
  constructor(){
    super()  // 不管父类有没有constructor 子类如果有 都必须用super()来继承父类的constructor
  }
  study(){
    console.log(this)
    this.swim()
  }
}

const son = new Son()
son.study()  // 为什么这里的swim方法子类没有确可以用呢? 

类中的this总是指向调用它的对象

4.3 普通函数中的this和构造函数的this的区别

function Test(){
  this.a = 1
  this.b = 2
  console.log(this) //Test
}
function test(){
  this.a = 1
  console.log(this)  // window
}
test()

构造函数中的this是指向实例的对象,而普通函数的this是指向window。

4.4 箭头函数中的this

var a = 2 
var obj = {
  a:1
}
const test = ()=>{
  console.log(this.a)// 2
}
test.call(obj)   // 箭头函数忽略任何形式的this指向的改变

我们可以看到实际上通过call可以改变this的指向,理论上来说此时函数内部的this应该指向obj这个对象,但是箭头函数会忽略任何形式的this指向的改变。

4.5 this指向到底如何找?

箭头函数的this总是指向外层非箭头函数的作用域的this指向 一层层网上找

obj.test = ()=>{
  console.log(this)  //window  
}

该箭头函数的this向外层找就是window

obj.test1 = function(){
  var t = ()=>{
    console.log(this)  //obj   
  }
  t()
}

该箭头函数的this向外层找就是function(){} 所以指向obj

obj.test2 = function(){
  setTimeout(()=>{
    console.log(this)  // obj
  },100)
}

该箭头函数的this向外层找就是function(){} 所以指向obj

obj.test2 = function(){
  setTimeout(function(){
    console.log(this)  // window
  },100)
}

因为this在function(){}所以不需要往外层去找,但是定时器函数中的this指向的是window

obj.test3 = function(){
  var t1 = ()=>{
    var t2 = ()=>{
      console.log(this) // obj
    }
    t2()
  }
  t1()
}

该箭头函数的this向外层找是箭头函数在往外层是function(){} 所以指向obj

obj.test4 = function(){
  console.log(this)  // obj
  var t1 = function(){
  console.log(this) // window
    var t2 = ()=>{
      console.log(this) // window
    }
    t2()
  }
  t1()
}

该箭头函数的this向外层找就是函数t1,函数t1任何的指向,所以指向window。

var obj2 = {
  a:1,
  test3:function(){
    console.log(this)  // obj2
    function t(){
      console.log(this)  // window 
    }
    t()
  }
}

function t(){}没有任何指向所以指向window,test3被obj2引用所以指向obj2

4.6 如何改变this指向

var obj = {
  a:1
}

var obj2 = {
  a:100
}
var a = 2

function test(b,c){
  console.log(this.a,b,c)
}

test()  // window调用的 所以this指向window
test.call(obj,3,4) // obj调用的 所以this.a 指向 obj.a
test.apply(obj,[3,4])  // apply与call的区别在于数组


var test1 = test.bind(obj,3,4)  // bind返回的是一个函数  且只会生效一次
test1()

var test2 = test1.bind(obj2,3,4)
test2()   // 不会改变指向 因为只生效一次

五.以上代码涉及到的其他问题

5.1 箭头函数不可以作为构造函数使用

var Fn = ()=>{
  console.log(this)
}
console.log(new Fn())

Uncaught TypeError: Fn is not a constructor