欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

一个函数理解js的this和闭包

程序员文章站 2024-02-24 18:33:34
一个函数理解js的this和闭包——详解debounce debounce函数,俗称防抖函数,专治input、resize、scroll等频繁操作打爆或其他资源。前端面试几乎...

一个函数理解js的this和闭包——详解debounce

debounce函数,俗称防抖函数,专治input、resize、scroll等频繁操作打爆或其他资源。前端面试几乎必考,当然肯定会做一些变化。


input here:

<script> var handler = function () { console.log(this, date.now()); } document.getelementbyid('input').addeventlistener('input', handler); </script>

现状

用户每次输入操作都会触发handler调用,性能浪费。

目标

用户一直输入并不触发handler,直到用户停止输入500ms以上,才触发一次handler。

前提是,不修改原有的业务代码,且尽量通用。

思路

settimeout实现计时 高阶函数,即function作为参数并且返回function

代码实现过程

第一版

function debounce(fn, delay) {
  return function () {
    settimeout(function () {
      fn();
    }, delay);
  }
}

给handler包上试试

document.getelementbyid('input').addeventlistener('input', debounce(handler, 500));

明显不可以!!这样写只不过将每次触发都延时了500ms,并没有减少触发次数。不过我们至少实现了高阶函数,不会破坏原有的业务代码了。那么接下来就试着减少触发次数。

思路就是每次触发先cleartimeout把之前的计时器清掉,再重新settimout。那么问题来了,第2次进来时,怎么获取到第1次的计时器,并清除呢?

第二版

function debounce(fn, delay) {
  var timer;
  return function () { // 闭包
    cleartimeout(timer);
    timer = settimeout(function () {
      fn();
    }, delay);
  }
}

试来试去,发现把timer放到“外面”最好(为什么不放到更外面?),每次调用进来,大家用的都是一个timer,完美。同时,我们的第一个主角登场了——闭包。

闭包

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。——百度百科

在计算机科学中,闭包(英语:closure),又称词法闭包(lexical closure)或函数闭包(function closures),是引用了*变量的函数。这个被引用的*变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。——*

网上可以找个很多关于闭包的概念与解释,估计越看越蒙。认识事物需要一个从具象到抽象的过程,以目前的情况来看,我们只要知道,“定义在一个函数(外函数)内部的函数(内函数),并且内函数访问了外函数的变量,这个内函数就叫做闭包”。

最关键的问题,闭包有什么用?从debounce这个例子,我们可以看到,闭包可以让每次触发的handler共享一个变量,通常用到高阶函数的地方,就会用到闭包。再举几个闭包的应用场景,比如给ajax请求加缓存、加锁,为一系列回调设置初始值,防止污染全局或局部变量等。可能这么说大家还是若有若无的,没关系,实践出真知,现实当中肯定会碰到能够应用闭包的地方的。我们继续debounce。

终于解决了触发频率的问题了。但是!细心的同学肯定发现了。我们handler里的console打印出来的this,是不一样的!!!之前的this是input结点,现在的this是window对象。这绝对是不行的,比如我想要在handler里打印input的value,现在怎么做呢?

第三版

function debounce(fn, delay) {
  // 1
  var timer;
  return function () { // 闭包
    // 2
    var ctx = this; // this上下文
    cleartimeout(timer);
    timer = settimeout(function () {
      // 3
      fn.apply(ctx); // this上下文调用
    }, delay);
  }
}

解决思路也简单,就是先把正确的this保存起来,我们在这里把this称为“上下文”,大家可以细细品味一下这个词。然后用apply(或call)重新制定一下fn的上下文即可。

上下文this

js的this是很善变的,谁调用它,它就指向谁,所以“上下文”这个词还是很贴切的。那么,为什么在2处能够得到正确的this呢?涉及到上下文切换的地方,一共有3处,已在上面代码中标了出来。我总结了一个两步定位this法:

第一步,哪个函数执行这行?

第二步,这个函数挂在哪?

我们来实践一下。

第1处:

我们需要先简单处理一下,debounce其实是挂在window全局上的,写全应该是window.debounce(handler, 500)。然后,这个debounce是立即运行的,并不是被监听触发的。让我们开始三步定位。哪个函数执行这行?window.debounce!这个函数挂在哪?window!确定了,this指向的window。

第2处:

先简单处理下,debounce(handler, 500)的执行结果是返回一个函数,所以下面两段代码基本上可以视为等价的

document.getelementbyid('input').addeventlistener('input', debounce(handler, 500));

document.getelementbyid('input').addeventlistener('input', function () { // 闭包
  // 2
  var ctx = this; // this上下文
  cleartimeout(timer);
  timer = settimeout(function () {
    // 3
    fn.apply(ctx); // this上下文调用
  }, delay);
});

这么一看,就具体多了。首先,由于回调是不能立即执行的,所以需要找个地方把它挂起来,或者叫存起来。addeventlistener是dom的方法,所以addeventlistener只能把回调挂在dom上,可以理解成input.handler = function(){}。那么答案也很容易了。哪个函数执行这行?input.handler!这个函数挂在哪?input!完成。

第3处:

settimeout是挂在window上的,所以在执行的时候,实际上是window.settimeout()。我们用伪代码模拟下settimeout的实现

window.settimeout = function(fn, delay){
  // 因为不能立即执行,所以要找个地方挂fn,就只能把fn转交给它的主子window
  // 假设window存fn的属性叫settimeouthandler,与input.handler类似
  window.settimeouthandler = fn;
  // 等待delay毫秒……
  window.settimeouthandler(); // 执行
}

这里跟dom的回调很像,因为fn也不是立即执行,所以只能把fn挂在自己的“主子”window那,等倒计时结束后才能够调用它。哪个函数执行的这行?window.settimeouthandler!这个函数挂在哪?window。搞定。

稳妥起见,我们再加一个例子

var obj = {
  test: function(){
    console.log(this);
  }
}
obj.test(); // obj
settimeout(obj.test,1000); // window

第一个立即执行,不用转交。哪个函数执行这行?obj.test!挂在哪?obj!

第二个非立即执行,需要转交window.settimeouthandler = obj.test。哪个函数执行这行?window.settimeouthandler!挂在哪?window!

debounce最终版

function debounce(fn, delay) {
  var timer;
  return function () { // 闭包
    var ctx = this; // this上下文
    var args = arguments;
    cleartimeout(timer);
    timer = settimeout(function () {
      fn.apply(ctx, args); // this上下文调用
    }, delay);
  }
}

最后,我们再把传参也解决一下(arguments是默认的存储所有传参的类数组对象,时间关系这里就不展开了),完成。

总结

debounce是一个很实用也很经典的功能函数,每一行代码都有丰富的内涵。与其类似的还有throttle,可以查查巩固一下。本文主要是想借debounce这个实用的函数引出js当中的两个比较难理解,的点this和闭包。说实话,这两个点想讲明白很难,更靠谱的办法是用大量的实践来消化。本文算是给各位同学种下一颗种子,以后碰到类似的情况时,能够很快的想起本文的内容,帮助自己更好的理解与感悟。