JS防抖(debounce)this绑定逻辑及两种实现写法差异咨询
参考实现代码
第一段可用于生产的通用防抖实现
function debounce(fn, delay) { var timer return function () { var context = this var args = arguments clearTimeout(timer) timer = setTimeout(function () { fn.apply(context, args) }, delay) } }
第二段存在上下文错误的防抖实现
export function debounce(fn, delay) { var timer; return function () { var args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(this, args); }, delay); }; }
1. var context = this语句的设计逻辑
该写法的核心作用是缓存符合调用预期的this上下文。
防抖函数执行后会返回一个闭包函数,这个返回的函数才是业务代码实际调用的入口:当它被绑定为DOM事件回调、作为对象方法调用时,它的this会指向符合JS调用规则的预期值——比如绑定在按钮点击事件上时this指向按钮DOM实例,作为组件方法调用时this指向组件实例。
而setTimeout传入的普通函数回调,是在延迟到期后被推入全局任务队列执行的,它自身的this在非严格模式下指向全局对象(浏览器环境为window),严格模式下为undefined,根本拿不到外层闭包函数调用时的正确上下文。提前在外层闭包中把this赋值给context变量,就是为了把正确的上下文留存下来,供延迟执行的回调使用。
2. fn.apply(context, args)的作用与通用场景下绑定this的必要性
这行代码是防抖函数保证通用性的核心逻辑,承担两个作用:
- 把待执行的原函数
fn的this,强制绑定为之前缓存的正确调用上下文context - 把调用防抖返回函数时传入的所有参数(存在类数组对象
arguments中),原封不动透传给原函数fn
作为全项目通用的工具函数,这一步的绑定是必须的。如果不做上下文和参数透传,会直接导致业务逻辑异常:比如事件回调里拿不到触发事件的DOM元素、组件方法里拿不到组件实例上的属性/方法、事件对象等参数丢失等问题,工具函数会完全失去通用性。
这里选择
apply而非call,是因为apply支持直接传入类数组格式的参数集合,不需要手动把arguments转成数组,在ES5及以下环境兼容性更好、写法更简洁。
3. 两段实现的核心差异
两段代码的本质差异只有一点:延迟到期执行原函数时的this指向完全不同。
- 第一段实现提前缓存了闭包函数调用时的正确上下文,执行原函数时把这个正确的上下文绑定上去,完全符合业务调用的预期,是兼容全环境的正确实现,可以直接作为通用工具函数使用。
- 第二段实现没有缓存外层的正确
this,直接在setTimeout的普通回调里取this,这个this和业务调用时的上下文没有任何关系:非严格模式下指向window,严格模式下为undefined,会导致原函数执行时上下文完全错乱,业务中使用会频繁出现“方法不存在”“变量未定义”的报错,是存在明显bug的实现。
*拓展:在ES6+环境中,可以利用箭头函数没有自身this、会继承外层作用域this的特性,省略手动缓存context的步骤,写法更简洁,效果和第一段实现完全一致:
export function debounce(fn, delay) { let timer; // 用剩余参数代替arguments,语义更清晰 return function (...args) { clearTimeout(timer); // 箭头函数继承外层返回函数的this,不需要额外缓存 timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }
内容的提问来源于stack exchange,提问作者tjdwlshwp

