JavaScript閉包實現(xiàn)函數(shù)返回函數(shù)詳解
前言
在JavaScript的世界里,閉包是一個既神秘又強大的特性。它既是JavaScript的一大難點,也是JavaScript的特色之一。閉包的運用貫穿于許多高級應(yīng)用中,可以說,掌握閉包,就能解鎖JavaScript編程的更多可能性。
一、閉包與變量作用域
要理解閉包,我們首先得從JavaScript的變量作用域講起。變量的作用域主要分為兩種:全局變量和局部變量。全局變量可以在代碼的任何地方被訪問,而局部變量則只能在其所在的函數(shù)內(nèi)部被訪問。JavaScript語言的特別之處在于,函數(shù)內(nèi)部可以直接讀取全局變量,但函數(shù)外部卻無法讀取函數(shù)內(nèi)部的局部變量。閉包就可以實現(xiàn)。
二、閉包的實現(xiàn)-函數(shù)返回函數(shù)
閉包通常是通過函數(shù)返回函數(shù)的方式來實現(xiàn)的。這種模式在JavaScript中非常常見,它不僅增強了代碼的靈活性,還為閉包的實現(xiàn)提供了便利。以下是一個簡單的例子,幫助你更好地理解閉包的實現(xiàn)方式:
// 定義一個函數(shù)用來打招呼 function greet(name) { return function () { console.log(`Hello, ${name}!`); }; } // 定義一個變量 接收 返回值 // 這里返回的是一個函數(shù) const greet1 = greet("小紅"); console.log(greet1) /** * ? () { console.log(`Hello, ${name}!`); } */ // 執(zhí)行 函數(shù) greet1(); // 輸出:Hello, 小紅!
在這個例子中,`greet` 函數(shù)接收一個參數(shù) `name`,并返回一個匿名函數(shù)。這個匿名函數(shù)在執(zhí)行時,會訪問其創(chuàng)建時所在的作用域鏈中的變量 `name`。當我們調(diào)用 `greet("小紅")` 時,返回的匿名函數(shù)就“捕獲”了變量 `name` 的值 `"Alice"`,并將其保存在閉包中。因此,當我們調(diào)用 `greet()` 時,它就會輸出 `"Hello, 小紅!"`。這就是閉包的神奇之處,它讓函數(shù)能夠記住并訪問其創(chuàng)建時的變量。
三、閉包的應(yīng)用場景
閉包的應(yīng)用場景非常廣泛,從簡單的問候函數(shù)到復(fù)雜的事件監(jiān)聽器、延遲任務(wù)和緩存功能,都可以看到閉包的身影。以下是一些常見的應(yīng)用場景:
1.封裝私有變量
閉包可以用來封裝私有變量,實現(xiàn)數(shù)據(jù)的封裝和隱藏。例如,我們可以創(chuàng)建一個用戶對象,通過閉包來封裝用戶的姓名和年齡:
function createUser(name, age) { return { getName: function () { return name; // 訪問閉包中的變量 }, getAge: function () { return age; // 訪問閉包中的變量 }, setAge: function (newAge) { age = newAge; // 修改閉包中的變量 } }; } const user = createUser("小明", 25); console.log(user.getName()); // 輸出:小明 console.log(user.getAge()); // 輸出:25 user.setAge(26); console.log(user.getAge()); // 輸出:26
在這個例子中,`createUser` 函數(shù)通過閉包封裝了用戶的姓名和年齡。外部代碼無法直接訪問這些私有變量,只能通過 `getName`、`getAge` 和 `setAge` 方法來獲取和修改它們的值。這種封裝方式不僅保證了數(shù)據(jù)的安全性,還提供了靈活的接口供外部代碼使用。
2.創(chuàng)建獨立的計數(shù)器
閉包可以用來創(chuàng)建獨立的計數(shù)器,每個計數(shù)器都有自己的狀態(tài),互不影響。例如:
function createCounter() { let count = 0; // 閉包中的狀態(tài)值 return function () { count += 1; // 每次調(diào)用時遞增 console.log(`Count: ${count}`); }; } const counter1 = createCounter(); counter1(); // 輸出:Count: 1 counter1(); // 輸出:Count: 2 counter2(); // 輸出:Count: 1 counter2(); // 輸出:Count: 2
在這個例子中,每次調(diào)用 `createCounter` 函數(shù)時,都會創(chuàng)建一個新的閉包,其中包含一個獨立的計數(shù)器狀態(tài) `count`。因此,`counter1` 和 `counter2` 是兩個獨立的計數(shù)器,它們的計數(shù)互不影響。
3.實現(xiàn)延遲任務(wù)
閉包還可以用來實現(xiàn)延遲任務(wù),例如:
// task:這是一個函數(shù),表示需要延遲執(zhí)行的任務(wù)。 // delay:這是一個數(shù)字,表示延遲的時間(單位為毫秒)。 function createDelayedTask(task, delay) { // 在函數(shù)內(nèi)部,定義了一個變量 timeoutId,用于存儲 setTimeout 返回的定時器 ID。 // 這個變量被閉包捕獲,因此可以在返回的對象方法中訪問和修改它。 let timeoutId; return { run: function () { timeoutId = setTimeout(task, delay); }, cancel: function () { clearTimeout(timeoutId); console.log("任務(wù)取消"); } }; } // createDelayedTask 返回一個對象,包含 run 和 cancel 方法, // 并將其賦值給變量 delayedTask。 // task:一個匿名箭頭函數(shù) () => console.log("執(zhí)行任務(wù)"),表示需要延遲執(zhí)行的任務(wù)。 // delay:延遲時間為 2000 毫秒(即 2 秒)。 const delayedTask = createDelayedTask(() => console.log("執(zhí)行任務(wù)"), 2000); // 在內(nèi)部,setTimeout 被調(diào)用,將傳入的任務(wù)函數(shù) () => console.log("執(zhí)行任務(wù)") 設(shè)置為在 2 秒后執(zhí)行。 delayedTask.run(); // 啟動任務(wù) /** 這里又調(diào)用了一個 setTimeout,將 delayedTask.cancel 方法設(shè)置為在 1 秒后執(zhí)行。 當 delayedTask.cancel 被調(diào)用時: clearTimeout(timeoutId) 被執(zhí)行,清除之前設(shè)置的定時器(即取消延遲任務(wù))。 打印消息 "任務(wù)取消"。*/ setTimeout(delayedTask.cancel, 1000); // 在 1 秒后取消任務(wù)
在這個例子中,`createDelayedTask` 函數(shù)通過閉包封裝了延遲任務(wù)的邏輯和狀態(tài)。`run` 方法用于啟動任務(wù),`cancel` 方法用于取消任務(wù)。通過閉包,我們可以將任務(wù)的狀態(tài)(如 `timeoutId`)保存起來,方便在需要時進行操作。
運行過程總結(jié)
時間線:
- 0 秒:調(diào)用
delayedTask.run()
,設(shè)置一個定時器,計劃在 2 秒后執(zhí)行任務(wù)(打印"執(zhí)行任務(wù)"
)。 - 1 秒:調(diào)用
delayedTask.cancel()
,清除定時器,取消任務(wù)。 - 2 秒:原計劃的任務(wù)不會執(zhí)行,因為定時器已被清除。
輸出結(jié)果:
- 在 1 秒后,控制臺會輸出
"任務(wù)取消"
。 - 2 秒后,不會輸出
"執(zhí)行任務(wù)"
,因為任務(wù)已被取消。
這種模式在實際開發(fā)中非常有用,例如:
- 在用戶操作頻繁的場景下,避免重復(fù)觸發(fā)某些操作(如搜索框的防抖功能)。
- 在需要延遲執(zhí)行任務(wù)但可能需要取消任務(wù)的場景中(如用戶取消操作或超時取消任務(wù))。
4.實現(xiàn)緩存功能
閉包還可以用來實現(xiàn)緩存功能,例如:
function createCache() { const cache = {}; // 閉包中的緩存對象 return { get: function (key) { return cache[key]; }, set: function (key, value) { cache[key] = value; } }; } const myCache = createCache(); myCache.set("name", "小軍"); console.log(myCache.get("name")); // 輸出:小軍
在這個例子中,`createCache` 函數(shù)通過閉包封裝了一個緩存對象 `cache`。通過 `set` 方法可以將數(shù)據(jù)存儲到緩存中,通過 `get` 方法可以從緩存中獲取數(shù)據(jù)。這種緩存功能在實際開發(fā)中非常有用,可以提高程序的性能。
5.leetcode.2715執(zhí)行可取消函數(shù)的解法
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ArrayWrapper Example</title> </head> <body> <script> /** * 創(chuàng)建一個可取消的函數(shù)執(zhí)行器 * @param {Function} fn - 需要延遲執(zhí)行的函數(shù) * @param {Array} args - 傳遞給 fn 的參數(shù)數(shù)組 * @param {number} t - 延遲時間(毫秒) * @return {Function} - 返回一個取消函數(shù),用于取消延遲執(zhí)行 */ var cancellable = function (fn, args, t) { let timeOutId; // 用于存儲 setTimeout 返回的定時器 ID // 取消函數(shù),用于清除定時器 function cancelFn() { clearTimeout(timeOutId); // 清除定時器,阻止 fn 執(zhí)行 } // 設(shè)置定時器,延遲 t 毫秒后執(zhí)行 fn timeOutId = setTimeout(() => { fn(...args); // 使用展開運算符將 args 作為參數(shù)傳遞給 fn }, t); // 返回取消函數(shù),供外部調(diào)用 return cancelFn; }; // 用于存儲執(zhí)行結(jié)果的數(shù)組 const result = []; // 示例函數(shù),將輸入?yún)?shù)乘以 5 const fn = (x) => x * 5; // 示例函數(shù)的參數(shù)和延遲時間 const args = [2], t = 20, cancelTimeMs = 50; // 記錄開始時間,用于計算延遲執(zhí)行的時間差 const start = performance.now(); // 日志函數(shù),記錄函數(shù)執(zhí)行的時間和返回值 const log = (...argsArr) => { const diff = Math.floor(performance.now() - start); // 計算從開始到現(xiàn)在的毫秒數(shù) result.push({ "time": diff, "returned": fn(...argsArr) }); // 將執(zhí)行時間和返回值存入 result }; // 創(chuàng)建一個可取消的延遲任務(wù) const cancel = cancellable(log, args, t); // 在 cancelTimeMs 毫秒后調(diào)用取消函數(shù),取消延遲任務(wù) const maxT = Math.max(t, cancelTimeMs); // 計算延遲時間和取消時間的最大值 setTimeout(cancel, cancelTimeMs); // 在延遲任務(wù)和取消任務(wù)之后,打印結(jié)果 setTimeout(() => { console.log(result); // [{"time":20,"returned":10}] }, maxT + 15); // 確保在所有任務(wù)完成后打印結(jié)果 </script> </body> </html>
詳細解釋:
設(shè)置定時器:
timeoutId = setTimeout(() => { fn(...args); // 使用 args 作為參數(shù)執(zhí)行 fn }, t);
這里使用 setTimeout
設(shè)置了一個定時器,延遲 t
毫秒后執(zhí)行 fn(...args)
。
setTimeout
返回一個唯一的 timeoutId
,這個 ID 用于后續(xù)的取消操作。
2.定義取消函數(shù):
function cancelFn() { clearTimeout(timeoutId); // 清除定時器,取消 fn 的執(zhí)行 }
cancelFn
是一個函數(shù),它的作用是調(diào)用 clearTimeout(timeoutId)
。
如果在 fn
執(zhí)行之前調(diào)用了 cancelFn
,clearTimeout
會取消對應(yīng)的定時器,fn
就不會被執(zhí)行。
返回取消函數(shù):
return cancelFn;
返回 cancelFn
是為了讓調(diào)用者能夠在需要的時候調(diào)用它。
如果調(diào)用者沒有調(diào)用 cancelFn
,定時器會正常觸發(fā),fn
會在延遲時間 t
后執(zhí)行。
如果調(diào)用者調(diào)用了 cancelFn
,clearTimeout
會取消定時器,fn
就不會被執(zhí)行。
. setTimeout
和 clearTimeout
的工作機制
setTimeout
:設(shè)置一個定時器,延遲 t
毫秒后執(zhí)行某個函數(shù)。它返回一個定時器的 ID(timeoutId
),這個 ID 用于后續(xù)的取消操作。
clearTimeout
:通過傳入定時器的 ID 來取消對應(yīng)的定時器。如果定時器已經(jīng)被觸發(fā)(即回調(diào)函數(shù)已經(jīng)開始執(zhí)行),clearTimeout
將不會有任何效果。
四、閉包的注意事項
雖然閉包非常強大,但在使用時也需要小心一些潛在的問題。例如,閉包可能會導(dǎo)致內(nèi)存泄漏,因為閉包會一直保存其創(chuàng)建時的作用域鏈中的變量,即使這些變量不再被使用,也不會被垃圾回收器回收。因此,在使用閉包時,我們需要確保及時釋放不再使用的變量,避免內(nèi)存泄漏。
五、總結(jié)
閉包是JavaScript中一個非常重要的特性,它通過函數(shù)返回函數(shù)的方式,讓函數(shù)能夠記住并訪問其創(chuàng)建時所在的作用域鏈中的變量。閉包不僅可以封裝私有變量,實現(xiàn)數(shù)據(jù)的封裝和隱藏,還可以創(chuàng)建獨立的計數(shù)器、實現(xiàn)延遲任務(wù)和緩存功能等。在實際開發(fā)中,閉包的應(yīng)用場景非常廣泛,掌握閉包的使用方法,可以讓你的代碼更加靈活和強大。當然,在使用閉包時,我們也要注意避免內(nèi)存泄漏等問題,合理地使用閉包,才能充分發(fā)揮其優(yōu)勢。
以上就是JavaScript閉包實現(xiàn)函數(shù)返回函數(shù)詳解的詳細內(nèi)容,更多關(guān)于JavaScript函數(shù)返回函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js實現(xiàn)base64、url和blob之間相互轉(zhuǎn)換的三種方式
Blob對象表示一個不可變、原始數(shù)據(jù)的類文件對象,Blob表示的不一定是JavaScript原生格式的數(shù)據(jù),下面這篇文章主要給大家介紹了關(guān)于js實現(xiàn)base64、url和blob之間相互轉(zhuǎn)換的三種方式,需要的朋友可以參考下2023-04-04layui實現(xiàn)把數(shù)據(jù)表格時間戳轉(zhuǎn)換為時間格式的例子
今天小編就為大家分享一篇layui實現(xiàn)把數(shù)據(jù)表格時間戳轉(zhuǎn)換為時間格式的例子,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09JavaScript基礎(chǔ)系列之函數(shù)和方法詳解
經(jīng)常談?wù)撈鸷瘮?shù)和方法,也常常搞不清楚它們之間的界限,經(jīng)常把兩個混用,這篇文章主要給大家介紹了關(guān)于JavaScript基礎(chǔ)系列之函數(shù)和方法的相關(guān)資料,需要的朋友可以參考下2021-09-09