bcjohn's blog
debounce 中的 this 指向
發布於: 2023-12-06 更新於: 2025-05-11 分類於: Javascript 閱讀次數: 

什麼是 debounce ?

debounce 是一種用於防止事件處理過於頻繁發生的技術,常用於處理使用者輸入,例如按鍵事件或搜尋框的輸入事件。

範例

以文字搜尋功能為例,使用者每次搜尋打的文字都會呼叫後端的 api 拿回資料,但通常使用者想搜尋的是最後打字的結果,而此時就適合使用 debounce 在一定時間過後才去真正的呼叫後端 api

以下面為例,搜尋框改變時,不會馬上觸發 onInput 方法,而是會等 500ms 後,如果搜尋框的文字沒有任何改變,才呼叫 onInput 方法

1
<input type="text" onInput="debounceInput" />
1
2
3
4
5
const onInput = (event) => {
console.log(event.target.value);
// call api...
};
const debounceInput = debounce(onInput, 500);

基礎版 - debounce

基礎的 debounce 是利用 setTimeout 延後 fn 函式的執行時間,等到 delay 時間過後才會實際呼叫 fn 函式執行

1
2
3
4
5
6
7
8
9
10
11
function debounce(fn, delay = 500) {
let timer = null;

return (...args) => {
if (timer) clearTimeout(timer);

timer = setTimeout(() => {
fn(...args);
}, delay);
};
}

添加 this 版 - debounce

以上的部分是 debounce 主要的功用,拿來避免事件頻繁的觸發,但這篇文章想講的是在 debounce 中的 this 指向

如果查看 debounce 相關的教學文章,會發現有些文章的 debounce 是這樣寫的:

1
2
3
4
5
6
7
8
9
10
11
12
function debounce(fn, delay = 500) {
let timer = null;

return function (...args) {
const self = this;
if (timer) clearTimeout(timer);

timer = setTimeout(function () {
fn.apply(self, args);
}, delay);
};
}

當使用 debounce 時如果沒有牽涉到 this 的指向,不論是 第一種基礎版 或是 第二種添加 this 版 的寫法都是一樣的,但如果牽扯到 this 的使用時就會有所差別

以下我們拿 vue2 的 option api 當作範例,來看看實際使用 debounce 時可能會遇到的 this 指向問題:

1
2
3
4
5
<template>
<div className="App">
<input type="text" @input="debounceInput" />
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
export default {
mounted() {
// 寫法 1.
this.debounceInput = debounce(this.onInput);

// 寫法 2.
this.debounceInput = debounce((event) => {
this.onInput(event);
});

// 寫法 3.
this.debounceInput = debounce(function (event) {
this.onInput(event);
});
},
methods: {
onInput(event) {
const newValue = event.target.value;
},
},
}
</script>

直接先講結論,寫法 1. 及 寫法 2. 不論是使用 基礎版 或是 添加 this 版 的 debounce,都是可以正常運作的

但 寫法 3. 如果搭配上 基礎版 - debounce 時,在 debounce 函式中沒有處理 this 的指向,this.onInput 中的 this 等於 undefined,因此也就沒有 onInput 方法可以呼叫,會直接報錯

1
2
3
4
5
// 寫法 3.
this.debounceInput = debounce(function (event) {
// 這裡的 this 會等於 undefined,導致直接報錯
this.onInput(event);
});

以下我們來看看這三種寫法,this 的指向有什麼樣的差別:

寫法 1.

第一種寫法 this.onInput 的 this 作用域是在 mounted 底下,所以 this 指向的是 vue instance,完全沒有問題

1
2
3
mounted() {
this.debounceInput = debounce(this.onInput);
},

寫法 2.

第二種寫法,在 debounce 裡使用了箭頭函式

1
2
3
4
5
mounted() {
this.debounceInput = debounce((event) => {
this.onInput(event);
});
},

箭頭函式在宣告它的地方的 this 是什麼,它的 this 就是什麼

箭頭函式沒有自己的 this,在箭頭函式中的 this 指向就是在宣告它時的 this,也就是 mounted 中的 this

1
2
3
4
5
6
7
mounted() {
console.log('mounted: this', this);
const arrowFn = (event) => {
// 這裡的 this 等同於第二行中 mounted 裡的 this
this.onInput(event);
};
},

所以第 5 行的 this 一樣會指向 vue instance 沒有問題


寫法 3.

第三種寫法在 debounce 中使用了傳統的 function

1
2
3
4
5
mounted() {
this.debounceInput = debounce(function (event) {
this.onInput(event);
});
},

在傳統的 function 執行環境下,this 的指向取決於函數怎麼被呼叫

所以接下來我們需要先釐清 debounce 中的 fn 函式如何被呼叫,才能夠知道 this.onInput 的 this 是指向誰

1
2
3
4
5
mounted() {
const fn = function (event) {
this.onInput(event);
};
},

這裡我們回頭來看一開始寫的 基礎版 - debounce,可以看到 fn 在第 8 行被呼叫,而這裡呼叫 fn 的時候並沒有傳遞 this 的指向,因此在嚴格模式下 fn 函式中的 this 會是 undefined

1
2
3
4
5
6
7
8
9
10
11
function debounce(fn, delay = 500) {
let timer = null;

return (...args) => {
if (timer) clearTimeout(timer);

timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
1
2
3
4
5
6
7
mounted() {
const fn = function (event) {
// 這裡的 this 等於 undefined
this.onInput(event);
};
this.debounceInput = debounce(fn);
},

Refs.
簡單呼叫 Callback Function (嚴謹模式)


那麼 添加 this 版 - debounce 中的 this 指向又是怎麼運作的呢?

在 debounce 函式中的第 5 行將 this 暫存到 self 變數裡,接著又在第 9 行利用 apply 將 fn 函式中的 this 指向 self,因此最後 fn 中 this 的指向將等同於第 5 行的 this

1
2
3
4
5
6
7
8
9
10
11
12
function debounce(fn, delay = 500) {
let timer = null;

return function (...args) {
const self = this;
if (timer) clearTimeout(timer);

timer = setTimeout(function () {
fn.apply(self, args);
}, delay);
};
}

而 debounce 中第 5 行的 this 又是從哪裡來的呢?

這裡我們可以看到呼叫 debounce 時指向的 this 是由 this.debounceInput 中的 this 來的,也就是 vue instance

1
2
3
4
5
mounted() {
this.debounceInput = debounce(function (event) {
this.onInput(event);
});
},
小結

this.debounceInput 中的 this 會傳遞到 debounce 函式第 5 行中的 this,this 賦值給 self 後又會經由第 9 行的 fn.apply 將 this 指向傳遞到 fn 中,因此最後 fn 中的 this.onInput 中的 this 也就等同於 this.debounceInput 中的 this

因此如果丟入 debounce 中的 fn 函式是採用傳統 function 的第三種寫法,這時 debounce 函式就必須正確的傳遞 this 指向 (添加 this 版 - debounce),才能夠使得 fn 函式中的 this 正常運作

Demo

結論

debounce 的教學文章有很多,但一直以來都不太懂為什麼有些文章使用 fn.apply 將 this 的指向傳入 fn 函式裡,而有些沒有。今天藉由 vue2 使用 debounce 的寫法,才瞭解傳遞 this 的指向是必要的

但現在的 vue3 或是 react16 後流行的 hooks 寫法,都幾乎不太會用到 this,因此可以放心不會遇到 寫法 3. 的這種 bug 了

參考資料

Day 21:箭頭函數 (Arrow Functions) 的 this 和你想的不一樣 (1)
淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂
What’s THIS in JavaScript ? [上]