面试题分析–详解闭包

大噶好,周五摸鱼时刻,我们来聊聊闭包。

闭包常常是面试中常见的一个题目,笔者在面试的时候也遇到了好几次,但每次都觉得自己回答的不够完善,今天呢,让我们来好好盘一下闭包这个东西。

一些相关概念

闭包与JavaScript中的词法作用域函数作用域密切相关。

词法作用域指的是变量的作用域由代码中变量声明的位置决定,而不是由代码执行时的上下文决定。

函数作用域指的是在函数内声明的变量只能在函数内部访问,外部无法直接访问。

闭包利用了词法作用域和函数作用域的特性,可以访问其外部函数中声明的变量,即使外部函数已经执行结束。这种特性使得闭包可以实现数据的封装和隐私,以及延长变量的生命周期。

另外,闭包也与JavaScript中的高阶函数和回调函数密切相关。

高阶函数是指可以接受函数作为参数或者返回一个函数的函数,而闭包常常作为高阶函数的返回值。

回调函数是指将一个函数作为参数传递给另一个函数,并在特定事件发生时执行。闭包经常用于实现回调函数,以及在异步编程中捕获和保存状态。

一些使用 Demo

1. 私有变量

通过闭包,可以创建私有变量和方法,从而实现数据的隐私和封装。这有助于构建更健壮的软件,因为实现细节更有可能以破坏性的方式发生变化。

function createCounter() {
  let count = 0;  // 这个变量是私有的,外部无法直接访问

  return {
    increment: function() {
      count++;
    },
    decrement: function() {
      count--;
    },
    getCount: function() {
      return count;
    }
  };
}

let counter = createCounter();

console.log(counter.getCount());  // 输出: 0
counter.increment();
counter.increment();
console.log(counter.getCount());  // 输出: 2
counter.decrement();
console.log(counter.getCount());  // 输出: 1

闭包实现私有变量的好处?

  1. 数据隐私和封装:闭包可以帮助实现数据的隐私和封装,通过隐藏内部状态和实现细节,可以避免外部直接访问和修改内部变量,从而减少了意外的数据篡改和错误的风险。
  2. 模块化开发:闭包可以用于创建模块化的代码结构,将相关的功能和数据封装在闭包内部,从而提高了代码的可维护性和可重用性。模块化开发有助于降低代码的耦合度,使得代码更易于维护和扩展。
  3. 减少全局变量:闭包可以帮助减少全局变量的使用,避免了全局命名冲突和不必要的全局污染,从而提高了代码的安全性和可维护性。
  4. 控制访问权限:通过闭包,可以实现只暴露特定接口的对象,从而控制外部对对象的访问和修改,提高了代码的安全性和可维护性。

2.部分应用和柯里化

通过闭包,可以实现部分应用,即将一个多参数的函数转换为一个单参数函数,从而实现更灵活和可复用的代码。

function multiply(x) {
  return function(y) {
    return x * y;
  };
}

const multiplyByTwo = multiply(2);
console.log(multiplyByTwo(5));  // 输出: 10

我们常见的面试题:“无限累加”也是柯里化和闭包的体现。

3. 事件处理程序和回调函数

闭包可以捕获外部作用域的变量,因此可以在事件处理程序和回调函数中使用闭包来访问外部作用域的变量,从而实现更灵活和可复用的代码

function createButton() {
  let clickCount = 0;

  const button = document.createElement('button');
  button.textContent = 'Click me';
  
  button.addEventListener('click', function() {
    clickCount++;
    console.log(`Button clicked ${clickCount} times`);
  });

  return button;
}

const button = createButton();
document.body.appendChild(button);

闭包的问题

闭包可能会有性能问题,并且由于闭包特殊的写法,理解成本可能比较高,会难以理解和调试,并且由于闭包可以修改外部数据,可能会有潜在的数据风险。

但是最常见的一个问题,就是闭包导致的内存泄露。

第一种是一直引用外部变量,第二种是在循环中创建闭包。

Case 1

function createLeak() {
  let data = new Array(1000000).join('*'); // 创建一个大型数据结构
  return function() {
    // 闭包持续引用外部变量 data
    console.log(data);
  };
}

// 创建闭包
const leak = createLeak();

// 虽然 createLeak 函数已经执行完毕,但闭包仍然持续引用 data
// 这会导致 data 无法被垃圾回收,从而造成内存泄漏

修复方式:

function createLeak() {
  let data = new Array(1000000).join('*'); // 创建一个大型数据结构
  return function() {
    // 闭包持续引用外部变量 data
    console.log(data);
    // 释放对外部变量的引用
    data = null;
  };
}

// 创建闭包
const leak = createLeak();

// 执行闭包
leak();

Case 2

function createClosures() {
  let closures = [];
  for (let i = 0; i < 10; i++) {
    closures.push(function() {
      // 闭包持续引用循环中的变量 i
      console.log(i);
    });
  }
  return closures;
}

// 创建闭包数组
const closures = createClosures();

// 由于每个闭包都持续引用循环中的变量 i,这些变量无法被释放,可能导致内存泄漏

修复方式:

function createClosures() {
  let closures = [];
  for (let i = 0; i < 10; i++) {
    (function(index) {
      closures.push(function() {
        // 闭包不再持续引用循环中的变量 i
        console.log(index);
      });
    })(i);
  }
  return closures;
}

// 创建闭包数组
const closures = createClosures();

// 执行闭包
closures.forEach(function(closure) {
  closure();
});

那么这就是本篇文章的全部内容啦~

如果您有补充可以和咱说下,咱正好学习一下.jpg

原文链接:https://juejin.cn/post/7312387695463825420 作者:阳树阳树

(0)
上一篇 2023年12月15日 下午5:02
下一篇 2023年12月15日 下午5:13

相关推荐

发表回复

登录后才能评论