前端基礎
-閉包是什麼?JS Closure 完整應用教學
this.web
閉包 Closure 一直是很重要的前端觀念,也是面試愛考題。有很多實務場景都有使用到閉包(Closure),像是 debounce、資料封裝、非同步處理等等,都很常看到閉包的身影,但對於程式新手而言,閉包不是那麼好理解,所以今天這篇文章,就想帶你由淺入深了解閉包 Closure!
閉包 Closure 是什麼?
用最簡單的一句話說就是:內部函數訪問它外部的變數,像以下程式碼,一個內部函數 inner,它在一個外部函數 outer 的裡面,並且它用到了它外部的變數:
function outer() {
let a = 1;
return function inner() {
console.log(a); // 訪問 inner 外部的變數
};
}
let closure = outer();
closure(); // 1
在 outer
函數內,我們先宣告變數 a 為 1,並且返回一個函數 inner
,inner
會在控制台打印變數 a。因為 inner
訪問了它外部的變數 a
,這樣就形成了一個閉包。
閉包 Closure 可以做什麼?
閉包最大的用處是做私有變數,舉例來說,我們現在要做很多個計數器 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 的回收機制。
閉包 Closure 的底層機制
為了節省記憶體,JavaScript 會自動把沒用到的變數回收,避免佔用記憶體,也就是 JS 的垃圾回收 Garbage Collection。通常一般的函數在執行結束後,裡面的變數就會被自動回收,例如:
function sayHi() {
const message = 'hi';
console.log(message);
}
sayHi(); // hi
當我們執行完 sayHi 函數後,message 就會被回收,不會存在記憶體了。
反過來說,如果某個變數再之後的程式碼還會用到,那就會持續留在記憶體中,
而上面 makeCounter 例子中,因為使用了閉包,JavaScript 就無法確定 counts 之後還會不會用到,所以就會一直保存在記憶體之中,造成閉包的發生。
閉包 Closure 的缺點
其實閉包的缺點正是它的優點造成的,因為變數不會被回收,會佔著記憶體,所以過度使用閉包會影響效能。如果我們確定未來不會再使用到變數,也可以使用以下程式碼來手動釋放記憶體:
couter1=null
閉包 Closure 的應用
閉包的應用很多,這邊講幾個最常出現的實際場景。
閉包的應用 1 - 防止頻繁的觸發 Debounce
第一個應用是防止頻繁觸發,有時候我們不希望某個函數在短時間內觸發多次,而是只觸發最後一次,這被稱為防抖(Debounce),例如滾動頁面的觸發的函數,就可以利用閉包解決:
function debounce(fn, delay) {
let timer = null;
// 返回匿名函數
return function () {
if (timer) {
// timer 第一次執行後會被保存在記憶體中,不會被回收
clearTimeout(timer); // 執行 clearTimeout 後才被回收
}
// 一段時間後觸發我們傳入的函數
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 來做到這件事情。
總結
總而言之,閉包就是內部函數訪問外部變數,閉包最常用的場景就是製作私有變數,常見的應用有防抖、節流和封裝函式庫。