看懂防抖节流函数及 arguments 对象

5,389 阅读4分钟

这篇文章的重点不在防抖和节流概念的描述,而是记录防抖和节流中让我困惑的逻辑,包括 arguments 对象。如有错误,恳请大佬指正。

防抖

var debounce = function(func, delay) {
  var timer = null;
  return function(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
};
window.addEventListener("scroll", debounce(func, delay));
  • addEventListener 的第二个参数是一个返回 scroll 事件要绑定的回调函数函数。scroll 事件需要绑定一个函数,所以 debounce 执行返回一个函数,绑定在 scroll 事件上。
  • addEventListener 绑定是一次性的。debounce 只在 scroll 事件回调被绑定的时候执行了一次,所以 timer 全局都只有一个。回调函数是闭包,所以能访问到 timer。
  • scroll 事件的回调函数要接收的参数用 ES6 中的 rest 参数统一存储到变量 args 中。
  • 为什么 func 中要用 apply 保留 this
    • 业务函数直接做回调的 this 不能因为加了一层 debounce 而有所改变。
    • scroll 事件的回调函数的 this 和 debounce 返回的函数中的 this 相同,也即 setTimeout 函数中的 this。
    • 如果 setTimeout 函数的回调函数是箭头函数,那么 func 的 this 和 setTimeout 函数的 this 相同,直接使用即可。如果是普通函数就需要外部保存一下再传进来使用(throttle的定时器写法)。

节流

注意点和防抖函数相同

时间戳实现
var throttle = (func, wait = 50) => {
  let lastTime = 0;
  return function(...args) {
    let now = +new Date();
    if (now - lastTime > wait) {
      lastTime = now;
      func.apply(this, args);
    }
  };
};
定时器实现
var throttle = function(func, delay) {
  var timer = null;
  return function() {
    var context = this;
    var args = arguments;
    if (!timer) {
      timer = setTimeout(function() {
        func.apply(context, args);
        timer = null;
      }, delay);
    }
  };
};

function handle() {
  // 业务逻辑函数
  console.log(Math.random());
}

window.addEventListener("scroll", throttle(handle, 1000));

arguments

概念

《js 高程 3.7.1 理解参数》 arguments 和形参的关系

  • 通过 arguments 可以获取到所有传递给函数的参数,无论是否有形参接收。所以命名参数并不是必须的。
  • arguments 是个类数组,有 length 属性,可以通过下标访问
  • 命名参数被修改,更新会同步到 arguments 相应位置(只在非严格模式下)。但它们并不访问相同的内存空间,只是值会同步。
  • 箭头函数里没有 arguments
  • 所有参数都通过值传递,不可能传递引用。
    • 即给一个函数传一个对象,函数中修改这个对象的引用不会影响之前的对象。修改对象中属性的值会影响之前的对象。
    • 之前的对象的指针复制了一份,给了这个参数。如果把这个参数的引用改了,那么就和之前的对象无关了。

项目中的应用

  • 代码中的 arguments 是 scroll 监听传给回调函数的参数。
  • 下面有多个debounce函数,区别在于arguments的写法。每个函数按照P1 P2 P3...的顺序看注释,都在解释arguments。
// html
<input type="text" id="inputDom">
<div id="list"></div>

// js
var inputDom = document.getElementById('inputDom');
inputDom.addEventListener("keyup", debounce(searchHandle));
const globalVar = 'globalVar';
/* 业务函数 */
function searchHandle(e, dom, data) {
    console.log(e);
    console.log(dom);
    console.log(data);
}

function debounce(func) {
    let listDom = document.getElementById("list");
    var timer = null;
    return function () {
        console.log(arguments);// keyup监听事件,默认传递给回调函数的参数。
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function () {// setTimeout() 和 第一个参数即回调函数 是不同的两层函数。// 这个回调函数没有默认传进来的arguments,因为它并不是被监听事件调用的。
            console.log(arguments);
            func(arguments[0][0], listDom, globalVar);
        }, 300, arguments);
    };
}

function debounce1(func) {
    return function () {
        console.log(arguments);// P1. keyup监听事件,默认传递给回调函数的参数。
        // ...略
        timer = setTimeout(function () {
            console.log(arguments);// P3. 这里是arguments对象。arguments[0]即传进来的P2
            func(arguments[0][0], tsgDom, mapTSG); // P4. arguments对象。arguments[0][0]是P1处arguments[0],即事件监听event。
        }, 300, arguments);// P2. 这个arguments是P1处的arguments,但是在这里只是个普通的命名参数,不是arguments对象。
    };
}

function debounce2(func) {
    return function () {
        console.log(arguments);// P1.
        var myParam = arguments;
        // ...略
        timer = setTimeout(function (myParam2) { // P3. 这里是回调函数接受的参数
            console.log(arguments);// P4. arguments[0]即myParam2
            func(arguments[0][0], tsgDom, mapTSG); // P5.
            func(myParam2[0], tsgDom, mapTSG); // P6.
        }, 300, myParam);// P2. 这里是setTimeout传递给回调函数的参数。
    };
}

function debounce3(func) {
    return function () {
        console.log(arguments);// P1.
        var myParam = arguments;
        // ...略
        timer = setTimeout(function () { // P3. 这里不接收
            console.log(arguments);// P4. 这里arguments的长度为0,因为该回调并没有接收到任何参数。
            func(myParam[0], tsgDom, mapTSG); // P5. 这里仍可以访问到myParam,因为闭包。
        }, 300);// P2. 这里不传递
    };
}

没有防抖节流,直接传参

window.addEventListener("scroll", function () {
    var otherParam = 'otherParam';
    searchHandle(arguments[0], otherParam); // arguments[0]是event
})

调用逻辑函数,需绑定 this

window.addEventListener("scroll", function () {
    var otherParam = 'otherParam';
    searchHandle.call(this, arguments[0], otherParam); // arguments[0]是event
})