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开发技术要点");

实现这些常见的函数可以帮助我们更好地理解修饰器的工作方式,并让我们了解它可以封装的逻辑类型。

函数修饰器是一种强大的工具,可以在不修改原始函数的情况下创建现有函数的变体。它们可以作为函数编程工具箱的一部分,用于重用公共逻辑。