一個函數理解js的this和閉包——詳解debounce

debounce應用場景模擬

debounce函數,俗稱防抖函數,專治input、resize、scroll等頻繁操作打爆瀏覽器或其他資源。前端面試幾乎必考,當然肯定會做一些變化。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Debounce Demo</title>
</head>

<body>
  <p>Input here:</p>
  <input type="text" id="input">
  <script>
    var handler = function () {
      console.log(this, Date.now());
    }
    document.getElementById('input').addEventListener('input', handler);
  </script>
</body>

</html>

現狀

用戶每次輸入操作都會觸發handler調用,性能浪費。

目標

用戶一直輸入並不觸發handler,直到用戶停止輸入500ms以上,才觸發一次handler。

前提是,不修改原有的業務代碼,且儘量通用。

思路

  1. setTimeout實現計時
  2. 高階函數,即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法:

第一步,是否立即執行?如果是,跳過第二步!

第二步,如果不是立即執行,它一定會被轉交給某個對象保管,看它被掛在了哪,或者說轉交給了誰!

第三步,這個執行函數掛在誰身上,誰就是this!

我們來實踐一下。

第1處:

我們需要先簡單處理一下,debounce其實是掛在window全局上的,寫全應該是window.debounce(handler, 500)。第一步,是立即執行的!跳過第二步!第三步,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上,所以this指向了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的回調非常像。第一步,不是立即執行;第二步,setTimeout是掛在window上的方法,所以只能轉交給window的某個方法保管(假設叫setTimeoutHandler,名字不重要);第三步,setTimeoutHandler掛在window上,所以this指向window。

穩妥起見,我們再加一個例子

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

第一個很簡單,立即執行,不用轉交。直接可以定位this指向了obj!

第二個非立即執行,雖然傳進去的是obj.test,實際上需要轉交給window.setTimeoutHandler保管,即window.setTimeoutHandler = obj.test。所以this指向的是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和閉包。說實話,這兩個點想講明白很難,更靠譜的辦法是用大量的實踐來消化。本文算是給各位同學種下一顆種子,以後碰到類似的情況時,能夠很快的想起本文的內容,幫助自己更好的理解與感悟。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章