js闭包的一些常见的使用场景

125 阅读8分钟

js闭包的一些常见的使用场景

保存变量状态和私有化变量和函数

function Counter() {  
    let count = 0; // 私有变量  
  
    return {  
        increment: function() {  
            count++;  
        },  
        getCount: function() {  
            return count;  
        }  
    };  
}  
  
const counter = Counter();  
counter.increment();  
counter.increment();  
console.log(counter.getCount()); // 输出: 2

在这个例子中,Counter 函数返回了一个对象,该对象有两个方法:incrementgetCount。这两个方法都可以访问和操作 count 变量,但由于 count 是在 Counter 函数的作用域内声明的,它对于外部是不可见的,实现了变量的私有化。同时,每次调用 Counter() 都会创建一个新的 count 变量实例,从而保存了每个计数器的状态。

用于事件处理和回调函数

function createButtonHandler(message) {  
    return function handleClick() {  
        alert(message);  
    };  
}  
  
const button1 = document.getElementById('button1');  
const button2 = document.getElementById('button2');  
  
button1.addEventListener('click', createButtonHandler('Hello, Button 1!'));  
button2.addEventListener('click', createButtonHandler('Hello, Button 2!'));

在这个例子中,createButtonHandler 函数接受一个 message 参数,并返回一个 handleClick 函数。当按钮被点击时,会触发 handleClick 函数,并显示对应的 message。由于闭包的作用,每个按钮都有自己的 message,即使 createButtonHandler 函数已经执行完毕。

使用闭包封装“类”

在JavaScript中,虽然没有传统意义上的类(class)的概念(直到ES6引入class关键字作为语法糖),但我们可以使用函数和闭包来模拟类的行为,以及实现模块化的代码组织。以下是一个使用闭包来封装“类”和模块的例子:

function MyClass(initialValue) {  
    // 私有变量  
    var privateVariable = initialValue;  
  
    // 私有方法  
    function privateMethod() {  
        console.log('This is a private method.');  
    }  
  
    // 公有方法,通过闭包可以访问私有变量和私有方法  
    return {  
        getPrivateVariable: function() {  
            return privateVariable;  
        },  
        setPrivateVariable: function(newValue) {  
            privateVariable = newValue;  
        },  
        publicMethod: function() {  
            console.log('This is a public method.');  
            privateMethod(); // 调用私有方法  
        }  
    };  
}  
  
// 创建MyClass的实例  
var myInstance = MyClass(10);  
  
// 访问公有方法  
myInstance.publicMethod(); // 输出: This is a public method. This is a private method.  
  
// 访问和修改私有变量  
console.log(myInstance.getPrivateVariable()); // 输出: 10  
myInstance.setPrivateVariable(20);  
console.log(myInstance.getPrivateVariable()); // 输出: 20

在这个例子中,MyClass 是一个构造函数,它创建并返回一个对象,这个对象包含了公有方法,这些方法通过闭包可以访问到在 MyClass 作用域内定义的私有变量和私有方法。这样,我们就模拟出了一个带有私有成员的“类”。

使用闭包实现模块化

模块化通常意味着将代码划分为多个独立的部分,每个部分都有其自己的作用域和命名空间,以避免全局命名冲突,并提供更好的代码组织。在JavaScript中,可以使用IIFE(Immediately Invoked Function Expression,立即调用的函数表达式)和闭包来实现模块化。

// 使用IIFE和闭包实现模块  
var myModule = (function() {  
    // 私有变量  
    var privateVar = 'Hello, Module!';  
  
    // 私有函数  
    function privateFunction() {  
        console.log('This is a private function in the module.');  
    }  
  
    // 公有接口  
    return {  
        getPrivateVar: function() {  
            return privateVar;  
        },  
        publicFunction: function() {  
            console.log('This is a public function in the module.');  
            privateFunction(); // 调用私有函数  
        }  
    };  
})();  
  
// 使用模块  
myModule.publicFunction(); // 输出: This is a public function in the module. This is a private function in the module.  
console.log(myModule.getPrivateVar()); // 输出: Hello, Module!

在这个模块化的示例中,我们创建了一个IIFE,它立即执行并返回一个对象。这个对象暴露了一些公有方法和属性,它们可以访问模块内部的私有变量和函数。由于IIFE创建了一个独立的作用域,因此模块内部的变量和函数不会污染全局作用域,也不会与其他模块发生冲突。

闭包实现函数式编程和柯里化

闭包在函数式编程中扮演着重要的角色,其中一个常见的应用是实现柯里化(Currying)。柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。这样做可以使函数的调用更加灵活,也便于函数的组合。

以下是一个使用闭包实现柯里化的JavaScript代码示例:

// 原始函数,接受两个参数并返回它们的和  
function add(x, y) {  
    return x + y;  
}  
  
// 柯里化函数,返回一个闭包  
function curryAdd() {  
    let args = [];  
      
    return function nextCurryAdd(...newArgs) {  
        args = args.concat(newArgs);  
          
        if (args.length >= 2) {  
            // 如果已经收集到足够的参数,就调用原始函数并返回结果  
            return add(...args);  
        } else {  
            // 否则返回一个新的闭包,继续收集参数  
            return nextCurryAdd;  
        }  
    };  
}  
  
// 使用柯里化后的函数  
const curriedAdd = curryAdd();  
  
console.log(curriedAdd(1)(2)); // 输出: 3  
console.log(curriedAdd(10)(20)); // 输出: 30  
  
// 也可以一次性传入所有参数  
console.log(curriedAdd(5, 5)); // 输出: 10

在这个例子中,curryAdd 函数返回了一个闭包 nextCurryAdd。这个闭包维护了一个 args 数组来收集参数。每次调用 nextCurryAdd 时,都会将新的参数添加到 args 数组中。如果 args 数组中的参数数量达到或超过两个,就调用原始的 add 函数并返回结果;否则,返回一个新的闭包以继续收集参数。

柯里化有很多好处,比如可以延迟计算、创建可复用的函数、简化高阶函数的使用等。在函数式编程中,柯里化是一种非常有用的技术。

此外,闭包也常用于实现高阶函数,比如 mapfilterreduce 等,这些函数接受一个函数作为参数,并对数组或其他集合执行某种操作。这些高阶函数利用闭包来记住传入的函数,并在集合的每个元素上调用它。这些高阶函数是函数式编程风格中非常常见的模式。

闭包解决循环中异步问题

在JavaScript中,循环中的异步操作经常会导致一些问题,因为循环变量在异步回调执行时可能已经改变。闭包可以帮助我们解决这个问题,通过创建每个迭代中特有的环境来捕获循环变量的当前值。

以下是一个使用闭包解决循环中异步问题的代码示例,假设我们有一个数组,我们想要对每个元素执行异步操作(例如,使用setTimeout模拟异步操作):

function asyncOperation(item, index, callback) {  
    // 模拟异步操作,比如网络请求或定时器  
    setTimeout(function() {  
        console.log(`异步操作完成,当前元素是:${item},索引是:${index}`);  
        callback(null, item); // 假设这是一个异步API的回调,带有错误和结果  
    }, 1000 * index); // 每个操作之间延迟递增,以便观察效果  
}  
  
function processArray(array, callback) {  
    array.forEach(function(item, index) {  
        // 使用闭包来捕获当前循环的索引和元素  
        (function(item, index) {  
            asyncOperation(item, index, function(err, result) {  
                if (err) {  
                    console.error('异步操作出错:', err);  
                } else {  
                    console.log('处理结果:', result);  
                }  
                  
                // 当所有异步操作都完成时,调用callback  
                if (index === array.length - 1) {  
                    callback();  
                }  
            });  
        })(item, index); // 立即调用闭包,并传入当前循环的item和index  
    });  
}  
  
// 示例数组  
var array = ['a', 'b', 'c', 'd', 'e'];  
  
// 当所有异步操作完成后调用的函数  
function allOperationsDone() {  
    console.log('所有异步操作完成!');  
}  
  
// 开始处理数组  
processArray(array, allOperationsDone);

在这个例子中,processArray 函数遍历数组,并对每个元素调用 asyncOperation 函数。由于 asyncOperation 是异步的,我们不能直接在 forEach 的回调中等待它完成。相反,我们使用一个立即调用的函数表达式(IIFE)来创建一个闭包,该闭包捕获了当前循环的 itemindex。这样,每个异步回调都有自己的 itemindex 值,即使循环已经继续到下一个迭代。

当所有异步操作都完成时,我们检查是否达到了数组的最后一个元素,并在那时调用 allOperationsDone 回调。这确保了只有在所有异步操作都完成后,才会执行完成操作。

闭包实现缓存和记忆化等功能

闭包在JavaScript中常被用于实现缓存(也称为记忆化)功能,即存储函数之前计算过的结果,以避免重复计算相同的输入。这对于那些计算开销较大且结果可以重复使用的函数特别有用。

以下是一个使用闭包实现缓存功能的代码示例:

function createCachedFunction(fn) {  
    let cache = {}; // 创建一个缓存对象  
      
    return function cachedFn(...args) {  
        const key = JSON.stringify(args); // 将参数转换为字符串作为缓存键  
          
        if (cache[key]) {  
            // 如果缓存中已有结果,则直接返回  
            console.log(`从缓存中获取结果: ${cache[key]}`);  
            return cache[key];  
        } else {  
            // 否则,计算函数结果,并存入缓存  
            const result = fn.apply(this, args);  
            cache[key] = result;  
            console.log(`计算并缓存结果: ${result}`);  
            return result;  
        }  
    };  
}  
  
// 假设我们有一个计算斐波那契数列的函数  
function fibonacci(n) {  
    if (n <= 1) return n;  
    return fibonacci(n - 1) + fibonacci(n - 2);  
}  
  
// 使用闭包创建一个缓存版本的斐波那契函数  
const cachedFibonacci = createCachedFunction(fibonacci);  
  
// 调用缓存版本的斐波那契函数  
console.log(cachedFibonacci(5)); // 第一次计算,结果会被缓存  
console.log(cachedFibonacci(5)); // 第二次计算,直接从缓存中获取结果

在这个例子中,createCachedFunction 是一个工厂函数,它接受一个函数 fn 作为参数,并返回一个新的函数 cachedFn。这个新函数使用闭包来维护一个 cache 对象,该对象用于存储之前计算过的结果。当 cachedFn 被调用时,它首先检查缓存中是否已经有对应的结果。如果有,就直接返回缓存中的结果;如果没有,就计算函数 fn 的结果,并将结果存入缓存中,然后返回。

通过这种方式,我们可以避免对相同的输入进行重复计算,从而提高程序的性能。

注意:在上面的代码中,我们使用 JSON.stringify(args) 将函数参数转换为字符串作为缓存键。这适用于简单的参数类型,如数字、字符串或简单的对象。对于包含循环引用或函数等复杂类型的参数,这种方法可能不适用,因为它可能导致错误或不可预测的行为。在实际应用中,你可能需要根据具体情况设计更健壮的缓存键生成策略。