目錄
?一、閉包是什么?概念
二、閉包為什么存在?作用
1. 創建私有變量
2. 實現數據封裝與信息隱藏
3. 模擬私有方法
4. 保存函數執行時的狀態
5. 回調函數和事件處理
6. 模塊化編程
7. 懶加載與延遲執行
?三、閉包怎么用?實踐+業務場景
1. 封裝私有變量
2. 延遲執行(定時器、異步回調)
3. 事件監聽和回調函數
5. 防抖和節流
業務場景:權限控制和角色管理
四、深入底層了解閉包的運行原理(難度指數????)
?一、閉包是什么?概念
閉包是指 函數可以“記住”并訪問定義時的作用域,即使這個函數在外部被調用時,依然能訪問到其定義時的父函數的局部變量。
-
父函數和子函數:
- 閉包通常發生在一個函數(父函數)內部定義了另一個函數(子函數),且子函數可以訪問父函數的局部變量。
-
通過
return
暴露子函數:- 當父函數返回子函數時,子函數就形成了閉包。因為子函數不僅僅是返回的函數,它還“記住”了父函數的作用域。
-
作用域鏈和內存管理:
- 通常,父函數的局部變量在父函數執行完畢后會被銷毀,但由于閉包的存在,這些局部變量會被保留在內存中,直到閉包不再被引用。
- 閉包使得父函數的局部變量不被銷毀,同時也避免了全局作用域的污染,因為它們只在閉包內部可見。
二、閉包為什么存在?作用
1. 創建私有變量
閉包最常見的作用之一是實現 私有變量。在 JavaScript 中,變量通常是公開的,任何函數都能訪問它們。而閉包允許我們創建只能通過特定函數訪問的私有變量,這樣就可以避免外部代碼隨意訪問或修改它們。
- 示例:
function createCounter() {let count = 0; // 這是一個私有變量return {increment: function() {count++;console.log(count);},decrement: function() {count--;console.log(count);},getCount: function() {return count;}};
}const counter = createCounter();
counter.increment(); // 輸出: 1
counter.increment(); // 輸出: 2
console.log(counter.getCount()); // 輸出: 2
// count 變量是私有的,外部無法直接訪問
在這個例子中,count
變量通過閉包被封裝在 createCounter
函數中,外部無法直接訪問和修改它,只有通過 increment
、decrement
和 getCount
方法才能操作它。
2. 實現數據封裝與信息隱藏
閉包提供了數據封裝的能力,可以將狀態和行為封裝在一個函數內部,并通過暴露的接口與外部進行交互。這有助于信息隱藏,防止外部代碼不小心或惡意地修改內部數據。
-
示例:
function bankAccount(initialBalance) {let balance = initialBalance; // 私有變量return {deposit: function(amount) {balance += amount;console.log(`Deposited: $${amount}`);},withdraw: function(amount) {if (balance >= amount) {balance -= amount;console.log(`Withdrew: $${amount}`);} else {console.log('Insufficient funds');}},getBalance: function() {return balance;}};
}const myAccount = bankAccount(1000);
myAccount.deposit(500); // Deposited: $500
myAccount.withdraw(200); // Withdrew: $200
console.log(myAccount.getBalance()); // 1300
// 不能直接訪問或修改 balance
這里的 balance
變量在 bankAccount
函數的作用域內被封裝,外部無法直接訪問或修改它,只有通過 deposit
、withdraw
和 getBalance
方法才能與其交互。
3. 模擬私有方法
除了私有變量,閉包也可以用來模擬 私有方法。你可以將某些功能封裝在閉包內部,外部只能通過公開的方法調用它們,從而達到隱藏細節、減少外部依賴的目的。
-
示例:
function car(model) {let speed = 0; // 私有變量function accelerate() {speed += 10;console.log(`Accelerating... Speed is now ${speed} km/h`);}return {start: function() {console.log(`${model} is starting`);accelerate();}};
}const myCar = car('Toyota');
myCar.start(); // Toyota is starting// Accelerating... Speed is now 10 km/h
在這個例子中,accelerate
函數是私有的,外部無法直接調用它,只有通過 start
方法間接調用。
4. 保存函數執行時的狀態
閉包能夠保持其外部函數的執行上下文,即使外部函數已經執行完畢。這樣,我們可以保存函數的 狀態,在后續的調用中繼續使用這些狀態。這對于處理 異步操作 或 回調函數 中的狀態非常有用。
- 示例:
function makeAdder(x) {return function(y) {return x + y; // 閉包可以記住 x 的值};
}const add5 = makeAdder(5);
console.log(add5(10)); // 15
const add10 = makeAdder(10);
console.log(add10(10)); // 20
在這個例子中,makeAdder
返回的函數是一個閉包,它“記住”了 x
的值。即使 makeAdder
執行結束后,x
仍然在閉包中保存,并且在后續的調用中可以使用它。
5. 回調函數和事件處理
在前端開發中,閉包廣泛應用于 事件處理 和 異步回調。它們能夠保持對外部數據(如事件觸發時的狀態、函數參數等)的訪問,即使在異步操作完成后,閉包仍然能夠訪問這些數據。
- 示例:事件處理中的閉包
function setupButton() {let counter = 0; // 閉包中的私有狀態document.getElementById('myButton').addEventListener('click', function() {counter++;console.log(`Button clicked ${counter} times`);});
}setupButton();
在這個例子中,事件回調函數可以訪問 counter
變量,它即使在 setupButton
函數執行完畢后仍然保持狀態。
6. 模塊化編程
閉包幫助我們將代碼分成獨立的模塊,每個模塊有自己的私有數據和方法。這樣不僅可以避免全局命名沖突,還可以提高代碼的可維護性和可復用性。
- 示例:
const counterModule = (function() {let count = 0; // 私有變量return {increment: function() {count++;console.log(count);},decrement: function() {count--;console.log(count);}};
})();counterModule.increment(); // 1
counterModule.decrement(); // 0
通過立即執行函數表達式(IIFE),counterModule
模塊中的 count
是私有的,外部無法直接訪問。閉包保證了每個模塊都有獨立的作用域和私有數據。
7. 懶加載與延遲執行
閉包還可以用于延遲執行函數和延遲計算,常見于懶加載場景。例如,某些數據或資源的加載操作可以通過閉包延遲到需要時再執行。
-
示例:
function fetchData() {let data = null;return function() {if (data === null) {console.log('Fetching data...');data = 'Some data'; // 模擬數據加載}return data;};
}const getData = fetchData();
console.log(getData()); // Fetching data... Some data
console.log(getData()); // Some data
這里,data
只在第一次調用 getData()
時被加載,之后就不會再進行加載操作,閉包保存了 data
的狀態。
?三、閉包怎么用?實踐+業務場景
1. 封裝私有變量
閉包常常用于封裝私有變量和創建數據的封裝(即模塊化編程)。在 JavaScript 中,通常沒有內建的私有變量機制,但閉包可以幫助你達到類似的效果。
- 示例:計數器
function createCounter() {let count = 0; // 私有變量return {increment: function() {count++;console.log(count);},decrement: function() {count--;console.log(count);},getCount: function() {return count;}};
}const counter = createCounter();
counter.increment(); // 輸出: 1
counter.increment(); // 輸出: 2
counter.decrement(); // 輸出: 1
console.log(counter.getCount()); // 輸出: 1
解析:
count
是一個私有變量,只能通過increment
、decrement
和getCount
方法訪問。- 外部無法直接訪問
count
,實現了數據的封裝。
2. 延遲執行(定時器、異步回調)
閉包經常用于處理異步操作和定時任務。例如,使用 setTimeout
或 setInterval
時,閉包允許你保留函數的執行上下文,從而延遲執行某些操作。
-
示例:延遲執行任務
function createDelayedTask(message, delay) {return function() {setTimeout(function() {console.log(message);}, delay);};
}const delayedTask = createDelayedTask('Hello, World!', 2000);
delayedTask(); // 2秒后輸出: Hello, World!
解析:
createDelayedTask
返回一個閉包,這個閉包可以記住其外部環境中的變量(如message
和delay
)。- 通過
setTimeout
延遲輸出message
,即使函數createDelayedTask
已經執行完畢。
3. 事件監聽和回調函數
閉包在事件監聽器和回調函數中非常常見。它可以讓回調函數訪問外部作用域中的變量,從而保持對數據的引用。
5. 防抖和節流
防抖和節流是常見的性能優化技巧。防抖(Debouncing)通常用于限制某些操作頻繁觸發(如輸入框中的搜索建議),而節流(Throttling)則是控制某些操作的觸發頻率(如窗口大小調整事件)。
業務場景:權限控制和角色管理
閉包可以用于權限管理和角色管理的場景中,通過閉包來封裝不同角色的權限信息,從而提供靈活的權限控制。
- 示例:權限管理
function createRoleChecker(role) {const permissions = {admin: ['read', 'write', 'delete'],user: ['read'],guest: []};return function(permission) {if (permissions[role] && permissions[role].includes(permission)) {console.log(`${role} has ${permission} permission.`);} else {console.log(`${role} does not have ${permission} permission.`);}};
}const adminChecker = createRoleChecker('admin');
const userChecker = createRoleChecker('user');
const guestChecker = createRoleChecker('guest');adminChecker('write'); // admin has write permission.
userChecker('write'); // user does not have write permission.
guestChecker('read'); // guest does not have read permission.
解釋:
createRoleChecker
返回一個閉包,它保存了角色的權限信息。- 每次調用
roleChecker
時,可以判斷特定角色是否擁有某個權限。 - 通過閉包,你可以靈活地管理角色和權限數據,避免權限數據暴露。
四、深入底層了解閉包的運行原理(難度指數????)
思考:
- 下面代碼輸出什么?
- 當
A(2)
執行時,局部變量x
和y
存儲在內存中,它們什么時候會被銷毀?
function A(y) {let x = 2;function B(z) {console.log(x + y + z);}return B;
}let C = A(2);
C(3);
上述執行的過程中到底在做什么:
-
A(2)
的調用:- JavaScript 引擎會為
A(2)
創建一個 執行上下文。 - 當調用
A(2)
時,y
賦值為2
,并且在A
內部創建了一個局部變量x = 2
和一個函數B(z)
。 - 然后,
A
返回了函數B
。
- JavaScript 引擎會為
-
形成閉包:
- 函數
B
在A
內部定義,因此它形成了閉包,能夠訪問A
內部的變量x
和y
,即使A
執行完畢,B
仍然可以訪問這些變量。 A(2)
執行完,JavaScript 會銷毀A
的執行上下文,但由于B
是通過閉包持有對A
作用域的引用,因此x
和y
并沒有被銷毀,它們的內存空間會保留下來。
- 函數
-
將
B
賦值給C
:- 通過
let C = A(2);
,變量C
被賦值為函數B
,且C
具有A
中的作用域(閉包),能夠訪問x
和y
。
- 通過
-
調用
C(3)
:- 當調用
C(3)
時,實際執行的是B(3)
。JavaScript 會創建C(3)
的執行上下文:即B(3)
的執行上下文。 - 在
B
中,x
和y
來自A
的作用域,z
來自B
的參數。因此,x + y + z
被計算為7
,并打印出來。 C(3)
調用結束,C(3)
的執行上下文即B(3)
的執行上下文會被銷毀。?但閉包仍然存在,因為B
被保存在變量C
中,并且C
仍然引用著閉包。當C
或B
被垃圾回收時,閉包才會被銷毀。因此,在C(3)
執行后,雖然B
的執行上下文棧幀被銷毀,但閉包中的內存(如x
和y
)會繼續存在,直到C
不再引用B
。
- 當調用
參考【易混概念】執行上下文和內存空間的聯系區別
留作業:如果閉包
B
被賦值給多個其他變量,這些變量會如何影響x
和y
的內存空間?評論區做答。
訂閱《前端通過之路》,助你一路通關!