前端基礎

-

JavaScript - Closure 閉包的詳解及實做應用

this.web

閉包 closure 一直是 JS 新手的第一座大山,也是面試愛考題,雖然網路上很多關於 JavaScript 閉包的文章,但我覺得都講得太難了,於是我用最簡單的方式,整理了一篇閉包 closure 的教學給你,整篇文章會分這幾個部分:

  1. 甚麼是閉包
  2. 閉包可以做什麼
  3. 閉包的缺點
  4. 閉包的應用

由淺入深帶你吃透 JS 閉包。那就讓我們開始吧!

JavaScript 閉包是什麼

用最簡單的一句話說就是:內部函數訪問外部函數的變數,仔細一點說就是,一個內部函數 inner,它在一個外部函數 outer 的裡面,並且它用到了它(函數 inner)外部的變數,例子如下:

function outer() {
  let a = 1;
  return function inner() {
    console.log(a); // 訪問 inner 外部的變數
  };
}

let closure = outer();
closure(); // 1


outer 我們先宣告變數 a 為 0,並且返回一個函數 innerinner 會在控制台打印變數 a。因為 inner 訪問了它外部的變數 a,這樣就形成了一個閉包。
再回去看那一句話:內部函數訪問外部函數的變數,是不是覺得很好理解了呢

接著我們來看看閉包可以幹嘛

閉包可以做什麼

閉包最大的用處是做私有變數,舉例來說,我們現在要做很多個記數器 counter,每個計數器都有自己的數字 counts,就可以利用閉包:

function makeCounter() {
  let counts = 0;
  return function () {
    console.log(++counts);
  };
}

const counter1 = makeCounter();
counter1(); // 1
counter1(); // 2

const counter2 = makeCounter();
counter2(); //1
counter1(); //3


你可以發現 counter1 counter2 不會互相影響,這就是閉包的好處。

那為什麼閉包可以製作私有變數呢?這就要提到 JS 的回收機制。

JS 的回收機制

簡單說 JS 會把沒用到的變數回收,如果之後的程式碼還會用到,那就會留在記憶體中,而一般的函數在執行結束後,裡面的變數就會被回收,例如:

function sayHi() {
  const message = 'hi';
  console.log(message);
}

sayHi();


當我們執行完 sayHi 函數後,message 就會被回收了。

但在 makeCounter 例子中,JS 無法確定 counts 之後還會不會用到,所以就會一直保存在記憶體之中,造成閉包的發生。

閉包的缺點

其實閉包的缺點正是它的優點造成的,因為變數不會被回收,會佔著記憶體,所以過度使用閉包會影響效能

不過我們可以使用以下程式碼來釋放記憶體。:

couter1=null

接著讓我們來看看閉包的實際應用場景,這部分會比較難一點點,但相信看完之後能讓你對閉包有更深的了解,如果有遇到什麼問題,也可以到我的 IG 私訊我喔!

閉包應用 1 - 防止頻繁的觸發 Debounce

第一個應用是防止頻繁觸發,有時候我們不希望函數在短時間內觸發多次,而是只觸發最後一次,這被稱為防抖(Debounce),例如滾動頁面的觸發的函數,就可以利用閉包解決:

function debounce(fn, delay) {
  let timer = null;

  // 返回匿名函數
  return function () {
    if (timer) {
      // timer 第一次執行後會被保存在記憶體中,不會被回收
      clearTimeout(timer);
      // 這裡才被回收
    }

    // 一段時間後觸發我們傳入的函數
    timer = setTimeout(() => {
      fn();
    }, delay);
  };
}

function scrollEvent() {
  console.log('觸發滾動事件');
}

const betterScroll = debounce(scrollEvent, 500);
document.addEventListener('scroll', betterScroll);


整段程式碼的解釋如下:

第一次滾動頁面時,觸發 betterScroll ,宣告外部變數 timer 值為 null,並回傳內部的匿名函數給 betterScroll,匿名函數內會設置 setTimeout,一段時間後執行 scrollEvent若 500 毫秒內又滾動一次,則 clearTimeout(timer),並重新設置 `setTimeout`,值到 500 毫秒內都沒有滾動才會執行 scrollEvent

這就是閉包最常見的第一個應用,用來防止用戶不小心多次點擊,造成一些意料之外的錯誤發生,接著來看看閉包的第二個應用~

閉包應用 2 - 防止頻繁的觸發 Throttle

和防抖相反,有時候我們只想觸發第一次,這稱為節流(Throttle)。實際程式碼如下:

function throttle(fm, interval) {
  // last 為上一次觸發的時間
  let last = 0;

  // 返回匿名函數
  return function () {
    // 紀錄現在時間
    let now = new Date();
    if (now - last >= interval) {
      last = now;
      fn();
    }
  };
}

function scrollEvent() {
  console.log('觸發滾動事件');
}

const betterScroll = throttle(scrollEvent, 500);
document.addEventListener('scroll', betterScroll);

第二個閉包應用和第一個功能相近,接下來講講非常常見,但可能沒有注意到的第三個閉包應用,封裝函式庫。

閉包應用 3 - 封裝函式庫

相信你或多或少有聽過 JQuery,它就是利用到了閉包的概念來封裝函式庫,避免變數全局汙染:

(function () {
  var jQuery = (window.$ = function () {
    // ...
  });
})();


它將 jQuery 掛載到 window.$ 上,並搭配閉包和 IIFE 來防止 $ 變數被系統回收,因為不會被回收,所以會一直存在記憶體中,這樣 $ 變數就成為了一個閉包,它可以在全局被訪問到,但內部的變數和方法卻是私有的,不會污染全局命名空間,達到函式庫封裝的效果。

如果你的專案有些方法是需要一個內部空間來避免汙染命名,就可以利用閉包搭配 IIFE 來做到這件事情。

小結

總而言之,閉包就是內部函數訪問外部變數,它可以用來製作私有變數,常見的應用有防抖、節流和封裝函式庫

希望這篇文章有幫助你學習閉包,我是請網這邊走,老樣子,下篇文章見!

相關系列文章