面试原理之闭包的优缺点和应用场景

1.关于闭包的概念

闭包是指一个函数能够访问并使用其父函数作用域中的变量,即使父函数已经执行完毕并返回了。闭包通常是由一个函数和对该函数的引用组成的。在 JavaScript 中,闭包是一种非常常见的编程模式,常用于实现模块化、私有变量、缓存、回调等功能。
闭包就像一把瑞士军刀,你以为你只是理解了区区八股文,但实际上你拥有了一个功能强大的工具,用的好可以解决各种棘手的问题,用的不好,可以让后来者来解决你制造的各种棘手的问题,并亲切的和你聊国粹的艺术。

2.简单示例

function counter() {
    let count = 0;
    return function () {
        count++;
        console.log(count);
    }
}

let increment = counter();
increment(); // 输出:1
increment(); // 输出:2
increment(); // 输出:3
increment = null; // 不使用闭包时,手动清除闭包

在上面的示例中,counter() 函数返回了一个匿名函数,该函数能够访问并使用 counter() 函数作用域中的 count 变量。当我们将 counter() 函数赋值给变量 increment 后,increment() 函数就是一个闭包了,每次调用 increment() 函数时,都会访问并更新 count 变量的值。

3.闭包常见的应用场景

1-实现模块化和私有变量

使用闭包可以实现模块化,将一些相关的函数和变量封装在一个函数中,并返回一个公共接口。这样就可以防止这些函数和变量被其他代码意外修改,提高了代码的安全性和可维护性。
在 JavaScript 中,没有原生的私有变量机制,但是可以通过闭包实现。将一个变量定义在函数内部,然后在函数返回的闭包中使用这个变量,这个变量就成为了私有变量,只能被闭包内的函数访问。

let counterModule = (function () {
    let count = 0;

    function increment() {
        count++;
        console.log('Count:', count);
    }

    function decrement() {
        count--;
        console.log('Count:', count);
    }

    return {
        increment,
        decrement
    };
})();

counterModule.increment(); // 输出:Count: 1
counterModule.increment(); // 输出:Count: 2
counterModule.decrement(); // 输出:Count: 1

counterModule = null; // 不使用闭包时,手动清除

在上面的示例中,我们使用了立即执行函数(IIFE)来创建一个闭包,并且在闭包内部定义了两个函数 increment()decrement(),用于对一个私有变量 count 进行增加和减少操作。然后我们返回一个包含这两个函数的对象,这样就可以通过该对象对外提供这些方法,从而实现模块化。
通过使用闭包,count 变量被定义在 createCounter() 函数内部,外部代码无法直接访问它,从而实现了私有变量的效果。我们可以将 count 变量隐藏在闭包内部,防止外部代码对其进行修改。这样可以有效地避免命名冲突和数据污染等问题,同时也可以提高代码的可维护性和安全性。

2-缓存数据

闭包可以用于实现数据缓存,当需要多次计算某个值时,可以将计算结果缓存起来,下次需要时直接使用缓存中的值,避免重复计算。

function cache(func) {
    const cached = {};
    
    return function(arg) {
      if (cached[arg]) {
        console.log('Using cached data for ' + arg);
        return cached[arg];
      } else {
        console.log('Calculating result for ' + arg);
        const result = func(arg);
        cached[arg] = result;
        return result;
      }
    }
  }
  
  function expensiveCalculation(num) {
    let result = 0;
    for (let i = 1; i <= num; i++) {
      result += i;
    }
    return result;
  }
  
  let cachedCalculation = cache(expensiveCalculation);
  
  console.log(cachedCalculation(10)); // 输出:Calculating result for 10 55
  console.log(cachedCalculation(10)); // 输出:Using cached data for 10 55
  console.log(cachedCalculation(20)); // 输出:Calculating result for 20 210
  console.log(cachedCalculation(20)); // 输出:Using cached data for 20 210
  cachedCalculation = null; //不使用闭包时,手动清除

在上面的示例中,我们定义了一个 cache() 函数,该函数接受一个参数 func,代表需要缓存的函数。在 cache() 函数内部,我们定义了一个 cached 对象,用于存储缓存数据。然后返回一个闭包函数,用于实际调用缓存的数据。
在闭包函数内部,我们首先判断 cached 对象中是否已经存在缓存数据。如果存在,就直接返回缓存数据,否则就调用传入的 func 函数进行计算,并将计算结果存储在 cached 对象中,最后返回计算结果。
通过这种方式,我们可以避免多次重复计算相同的值,从而提高代码的性能和效率。

3-实现回调函数

在 JavaScript 中,回调函数是一种常见的编程模式。使用闭包可以实现一些有趣的回调函数,例如事件监听、异步调用等。闭包可以让回调函数访问其所在函数的变量,从而实现更加灵活的编程。

function add(a, b, callback) {
  const result = a + b;
  callback(result);
}

function printResult(result) {
  console.log('The result is: ' + result);
}

add(1, 2, printResult); // 输出:The result is: 3

在上面的示例中,我们定义了一个 add() 函数,该函数接受三个参数:ab 分别代表两个加数,callback 代表回调函数。在函数内部,我们先计算出两个加数的和 result,然后将其作为参数传递给回调函数 callback
然后我们又定义了一个 printResult() 函数,用于打印计算结果。在调用 add() 函数时,我们将 printResult 函数作为回调函数传递给 add() 函数,当计算完成后,add() 函数会调用该回调函数并传递计算结果作为参数,然后我们就可以在回调函数内部处理计算结果。
通过这种方式,我们可以在异步操作完成后,执行一些额外的操作,从而实现更加灵活的编程方式。

4.闭包的缺点和解决办法

闭包的缺点主要有两个:

  1. 内存泄漏:由于闭包中的函数引用了外部函数的变量,而外部函数的作用域在函数执行结束后并不会被销毁,这就导致了闭包函数中的变量也无法被销毁,从而占用了内存空间。如果闭包被滥用,可能会导致内存泄漏的问题。
  2. 性能问题:闭包中的函数访问外部函数的变量需要通过作用域链来查找,而作用域链的长度决定了查找的速度。如果闭包层数较深,作用域链就会很长,从而影响了函数的执行效率。

为了解决闭包的这些问题,可以采取以下一些措施:

  1. 及时释放闭包:如果不再需要使用闭包,可以手动将其赋值为 null,从而释放闭包中占用的内存空间。
  2. 减少闭包层数:尽量减少闭包层数,避免作用域链过长,从而提高函数的执行效率。
  3. 使用立即执行函数:可以使用立即执行函数来避免闭包的内存泄漏问题。由于立即执行函数在执行结束后会被立即销毁,因此其中的变量也会被释放。
  4. 使用模块化编程:可以使用模块化编程来避免闭包的性能问题。在模块化编程中,每个模块都是一个独立的作用域,不会对全局作用域造成影响,从而避免了作用域链过长的问题。
function foo() {
  let count = 0;
  return function() {
    return ++count;
  };
}

let counter = foo(); // 创建闭包
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2

counter = null; // 释放闭包

你可能感兴趣的:(面试,javascript,前端)