Javascript閉包的4種高級用法

函數修飾器是一個高階函數,它將一個函數作為參數并返回另一個函數,并且返回的函數是參數函數的變體。

提高編程能力最好的方式就是去閱讀并學習開源框架或者腳本庫,今天我們就來學習underscore.jslodash.jsramda.js之類的庫中利用閉包原理實現函數修飾器,從中受益匪淺。

一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函數被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函數中訪問到其外層函數的作用域。在 JavaScript 中,每當創建一個函數,閉包就會在函數創建的同時被創建出來。

once()

在之前的文章中曾經寫過一個類似的函數once()。但是這里實現的功能有點不一樣,這里執行一次并返回函數的執行結果。

function once(fn) {
    let returnValue;
    let canRun = true;
    return function runOnce() {
        if (canRun) {
            returnValue = fn.apply(this, arguments);
            canRun = false;
        }
        return returnValue;
    };
}
function process(title) {
    console.log({
        title,
    });
}
const processonce = once(process);
const title = "DevPoint";
processonce(title);
processonce(title);
processonce(title);

上面代碼執行結果只會輸出一次:{ title: 'DevPoint' }

once()是一個返回另一個函數的函數。返回的函數runOnce()是一個閉包。同樣重要的是要注意原始函數是如何被調用的——通過傳入this的當前值和所有參數argumentsfn.apply(this, arguments)。可以應用的場景可以是搶購,如現在比較多的活動搶茅臺,可以減少瘋狂點擊發送請求。

after()

after(count, fn):創建僅在多次調用后才執行的函數方法。例如,當想要確保函數只在完成count次異步操作完成后才運行時,這個函數就非常實用。

function after(count, fn) {
    let runCount = 0;
    return function runAfter() {
        runCount = runCount + 1;
        if (runCount >= count) {
            return fn.apply(this, arguments);
        }
    };
}
function end() {
    console.log("異步操作結束了!");
}
const endAfter3Calls = after(3, end); // 定義在執行3次異步操作后執行函數logResult

setTimeout(() => {
    console.log("=>完成第一次異步操作");
    endAfter3Calls();
}, 3000);
setTimeout(() => {
    console.log("=>完成第二次異步操作");
    endAfter3Calls();
}, 2000);
setTimeout(() => {
    console.log("=>完成第三次異步操作");
    endAfter3Calls();
}, 6000);

上面代碼執行輸出結果如下:

=>完成第二次異步操作
=>完成第一次異步操作
=>完成第三次異步操作
異步操作結束了!

這個方法在前端需要做一系列動畫效果的時候很實用。

節流:throttle()

throttle(fn, wait):用于限制函數觸發的頻率,每個delay時間間隔,最多只能執行函數一次。一個最常見的例子是在監聽resize/scroll事件時,為了性能考慮,需要限制回調執行的頻率,此時便會使用throttle函數進行限制,也是我們常說的節流。

function throttle(fn, interval) {
    let lastTime;
    return function throttled() {
        const timeSinceLastExecution = Date.now() - lastTime;
        if (!lastTime || timeSinceLastExecution >= interval) {
            fn.apply(this, arguments);
            lastTime = Date.now();
        }
    };
}
function process() {
    console.log("DevPoint");
}
const throttledProcess = throttle(process, 1000);

for (let i = 0, len = 100; i < len; i++) {
    throttledProcess();
}

防抖:debounce()

debounce(fn, wait):可以減少函數觸發的頻率,但限制的方式有點不同。當函數觸發時,使用一個定時器延遲執行操作。當函數被再次觸發時,清除已設置的定時器,重新設置定時器。如果上一次的延遲操作還未執行,則會被清除。

function debounce(fn, interval) {
    let timer;
    const debounced = () => {
        clearTimeout(timer);
        const args = arguments;
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, interval);
    };
    return debounced;
}
function process() {
    console.log("DevPoint");
}
const delayProcess = debounce(process, 400);

for (let i = 0, len = 100; i < len; i++) {
    delayProcess();
}

throttle函數與debounce函數的區別就是throttle函數在觸發后會馬上執行,而debounce函數會在一定延遲后才執行。從觸發開始到延遲結束,只執行函數一次。

partial()

現在創建對所有函數都可用的partial()方法。這里使用了ECMAScript 6 rest參數語法,而不是參數 arguments 對象,下面實現連接數組和參數不是數組對象。

Function.prototype.partial = function (...leftArguments) {
    let fn = this;
    return function partialFn(...rightArguments) {
        let args = leftArguments.concat(rightArguments);
        return fn.apply(this, args);
    };
};
function log(level, message) {
    console.log(level + " :" + message);
}
const logInfo = log.partial("描述");
logInfo("DevPoint開發技術要點");

實現這些常見的函數可以幫助我們更好地理解修飾器的工作方式,并讓我們了解它可以封裝的邏輯類型。

函數修飾器是一種強大的工具,可以在不修改原始函數的情況下創建現有函數的變體。它們可以作為函數編程工具箱的一部分,用于重用公共邏輯。