深入探讨防抖函数中的 this 上下文
深入剖析防抖函数中的 this
上下文
最近我在研究防抖函数实现的时候,发现一个耗费脑子的问题,出现了令我困惑的问题。接下来,我将通过代码示例,深入探究这些现象背后的原理。
示例代码
function debounce(fn, delay) {
let timer = null;
console.log(1, 'this:', this);
return function(...args) {
console.log(2, 'this:', this);
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
console.log(3, 'this:', this);
fn.apply(this, args);
}, delay);
};
}
function debounce2(fn, delay) {
let timer = null;
console.log(11, 'this:', this);
return function(...args) {
console.log(22, 'this:', this);
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
console.log(33, 'this:', this);
fn.apply(this, args);
}, delay);
};
}
let obj = {
name: 'John',
sayName: function() {
console.log(this.name);
},
debouncedSayName: debounce(function() {
this.sayName();
}, 300),
debouncedSayName2: debounce2(function() {
this.sayName();
}, 300)
};
obj.debouncedSayName();
obj.debouncedSayName2();
现象与问题
运行上述代码后(假设在浏览器环境中),会得到如下输出:
这里有一个顺序问题让我困惑了一下:
- 顺序问题:为什么
1
打印完成后没有紧接着打印2
,而是先打印了11
?并且为什么输出顺序不是2 -> 3 -> 22 -> 33
呢?
为了彻底搞清楚这些现象,下面是对知识点的解析。
关键知识点解析
1. 函数定义时的 this
(打印 1
和 11
处)
在 debounce
和 debounce2
函数的定义里,console.log(1, 'this:', this)
和 console.log(11, 'this:', this)
中的 this
指向取决于函数的调用方式。由于这两个函数是直接定义在全局作用域下的,并没有通过某个对象来调用,所以在 JavaScript 中,this
默认指向全局对象 Window
(在 Node.js 环境中则是 global
)。
2. 内部返回函数中的 this
(打印 2
和 22
处)
在 return
的匿名函数中,console.log(2, 'this:', this)
和 console.log(22, 'this:', this)
里的 this
指向是由调用时的上下文决定的。
当我们执行 obj.debouncedSayName()
和 obj.debouncedSayName2()
时,这两个函数是作为 obj
对象的方法被调用的。根据 JavaScript 的规则,当函数作为对象的方法调用时,this
会指向调用该方法的对象,所以这两个地方的 this
都指向 obj
。
3. 定时器中的 this
(打印 3
和 33
处)
- 箭头函数中的
this
(console.log(3, 'this:', this)
):在debounce
函数的定时器回调中使用了箭头函数。箭头函数有一个重要的特性,就是它不会绑定自己的this
,而是继承自定义它的外部函数(这里是匿名函数)的this
。因此,这里的this
仍然指向obj
。 - 普通函数中的
this
(console.log(33, 'this:', this)
):在debounce2
函数的定时器回调中使用的是普通函数。普通函数的this
在调用时会动态绑定,而在定时器中,普通函数的this
默认指向全局对象Window
(在 Node.js 环境中是timeout
)。
顺序问题解析
观察输出顺序:1 -> 11 -> 2 -> 22 -> 3 -> 33
为什么不是 1 -> 2
?
当我们定义 obj
对象时,会通过 debounce
和 debounce2
函数生成 debouncedSayName
和 debouncedSayName2
属性。在这个过程中,debounce
和 debounce2
函数会被调用,于是就会执行到 console.log(1, 'this:', this)
和 console.log(11, 'this:', this)
。而 debounce
和 debounce2
返回的内部函数,要等到调用 obj.debouncedSayName()
和 obj.debouncedSayName2()
时才会执行。
为什么不是 2 -> 3 -> 22 -> 33
?
JavaScript 是单线程的编程语言,而 setTimeout
是异步操作。当遇到 setTimeout
时,它会将回调函数推入事件队列,等待主线程的同步任务全部执行完毕后再执行。因此,debouncedSayName
和 debouncedSayName2
的同步部分会先执行,分别打印 2
和 22
,然后才会将定时器的回调函数放入事件队列。最后,定时器的回调函数依次执行,分别打印 3
和 33
。
总结
通过这个例子,我们可以得出以下重要结论:
1. 箭头函数与普通函数的区别
- 箭头函数:箭头函数中的
this
继承自外层函数,这使得它非常适合用于需要保留上下文的场景。 - 普通函数:普通函数的
this
根据调用方式动态绑定,在不同的调用场景下,this
的指向可能会发生变化。
2. 异步任务的执行顺序
异步任务会被推入事件队列,只有当主线程的同步任务全部完成后,才会依次执行事件队列中的异步任务。
3. this
的指向受调用方式影响
如果函数作为对象的方法调用,this
会指向该对象;如果函数没有通过对象调用,this
通常指向全局对象(在严格模式下为 undefined
)。
希望通过这篇文章,你能更清晰地理解防抖函数中的 this
机制,在实际开发中避免因 this
指向问题而产生的错误。