“文艺复兴“--闭包

前言

老话常谈,闭包这个概念想必很多小伙伴都不陌生,小编今天把它拿出来"文艺复兴"一下,看看是否能产生什么新的学习感悟!

闭包是什么

这是MDN上的官方定义:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

当我们谈到闭包时,通常指的是函数能够访问其词法作用域之外的变量。这意味着函数可以“记住”在定义时可访问的变量,即使在不同的作用域中调用该函数时也能继续使用这些变量。

词法作用域:也称为静态作用域,指的是变量在写代码时的作用域,即在代码编写阶段就确定了变量的作用域范围。如下图:

00d90559227fa97b894e8f435bef4c08.jpg

接下来我们通过一个demo,来探讨闭包的工作原理。

function outerFunction(x) {
    // 在外部函数内定义一个内部函数
    function innerFunction(y) {
        // 内部函数访问了外部函数的变量 x
        return x + y;
    }
    // 外部函数返回内部函数
    return innerFunction;
}

// 创建一个闭包
var closure = outerFunction(5);

// 调用闭包
var result = closure(3);
console.log(result); // 输出 8

在这个示例中,outerFunction 是外部函数,它接受一个参数 x。在 outerFunction 中定义了内部函数 innerFunction,它接受一个参数 y,并返回 x + y 的结果。

外部函数 outerFunction 返回了内部函数 innerFunction,形成了闭包。

当我们调用 outerFunction(5) 时,它返回了一个闭包,这个闭包可以在后续的调用中使用,而且它记住了在创建时的 x 的值。因此,closure(3) 的结果是 5 + 3 = 8

简而言之,闭包就是即使在外部函数执行完毕并返回后,内部函数仍然可以访问和操作外部函数中声明的参数和变量。

闭包的应用场景

1. 保护数据

  • 用途:闭包可用于封装私有变量,提供公共接口来访问和修改这些变量,从而保护数据的安全性。

  • 实现方式:通过在函数内部定义局部变量,并返回一个包含访问和修改该变量的方法的对象,创建一个私有作用域。

  • 实现目的:增强数据的封装性和安全性,防止外部直接访问和修改数据。

function createCounter() {
  let count = 0;
  
  return {
    increment: function() {
      count++;
    },
    decrement: function() {
      count--;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.getCount()); // 输出: 0
counter.increment();
console.log(counter.getCount()); // 输出: 1

在这个例子中,createCounter 函数内部定义了一个私有变量 count,并返回了一个包含增加、减少和获取计数值的方法的对象。由于这些方法是在 createCounter 内部定义的,外部无法直接访问 count 变量,从而保护了数据的安全性。

2. 模块化开发

  • 用途:实现模块化的代码结构,将相关功能封装在一个独立的作用域内,避免全局命名冲突和污染。

  • 实现方式:使用立即调用的函数表达式(IIFE)和闭包结合,定义模块内部的私有变量和方法,并返回一个包含对外暴露方法的对象或函数。

  • 实现目的:封装和信息隐藏,使得代码更加清晰和可维护。

const module = (function() {
  let privateVar = 'I am private';

  function privateFunction() {
    console.log('This is private');
  }

  return {
    publicVar: 'I am public',
    publicFunction: function() {
      console.log('This is public');
    }
  };
})();

console.log(module.publicVar); // 输出: I am public
module.publicFunction(); // 输出: This is public

在这个例子中,使用立即调用的函数表达式(IIFE)创建了一个模块,并在模块内部定义了私有变量和方法,然后返回了一个包含公共接口的对象。这样做可以避免全局命名冲突,同时也能够更好地封装和组织代码。

3. 事件处理函数

  • 用途:确保在事件触发时能够访问到正确的变量值,实现事件处理函数的逻辑。

  • 实现方式:在事件处理函数内部使用闭包来访问外部作用域中的变量或函数。

  • 实现目的:使得内部的事件处理函数可以访问外部函数中定义的变量,即使外部函数已经执行完毕。

function setupCounter(elementId) {
  let count = 0;
  const element = document.getElementById(elementId);

  element.addEventListener('click', function() {
    count++;
    console.log('Count:', count);
  });
}

setupCounter('myButton');

setupCounter 函数中,通过闭包的方式在事件处理函数内部访问了 count 变量,确保了在每次点击事件触发时都能正确地访问和更新计数值。

4. 定时器和循环

  • 用途:在循环或定时器内部保存每次迭代的变量值,避免由于作用域链导致的变量共享或覆盖问题。

  • 实现方式:使用闭包保存每次迭代的变量值。

  • 实现目的:在循环或定时器中正确处理变量,避免作用域链导致的问题。

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log('Index:', i);
  }, 1000);
}

在使用 setTimeout 创建定时器时,由于 JavaScript 中的事件循环机制,使用 let 声明的变量会形成块级作用域,因此在闭包内部正确保存了每次迭代的变量值,避免了因为作用域链导致的问题。

5. 封装回调函数

  • 用途:捕获外部函数的执行上下文,确保在异步操作完成后能够执行回调函数,并访问到正确的上下文环境。

  • 实现方式:使用闭包保存外部函数的执行上下文,并传递给异步操作的回调函数。

  • 实现目的:处理异步操作的结果,确保能够访问到正确的上下文环境。

function fetchData(url, callback) {
  fetch(url)
    .then(response => response.json())
    .then(data => callback(data))
    .catch(error => console.error('Error:', error));
}

const processData = (function() {
  let counter = 0;

  return function(data) {
    counter++;
    console.log('Processed data:', data);
    console.log('Counter:', counter);
  };
})();

fetchData('https://api.example.com/data', processData);

fetchData 函数中,使用闭包保存了 counter 变量,并将其传递给 processData 函数作为回调。这样做确保了异步操作完成后能够访问到正确的上下文环境。

6. 函数柯里化

  • 用途:延迟执行函数,接受部分参数,并返回一个新的函数,用于接受剩余参数。

  • 实现方式:返回一个闭包,保存部分参数,并返回一个新的函数。

  • 实现目的:实现函数的柯里化,方便函数的复用和组合。

function add(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = add(5);
console.log(add5(3)); // 输出: 8

add 函数采用了柯里化的方式,通过返回一个闭包来保存部分参数,并返回一个新的函数来接受剩余参数,实现了参数延迟执行的效果。

7. 缓存结果

  • 用途:提高函数的执行效率,避免重复计算已经计算过的结果。

  • 实现方式:在闭包内部维护一个缓存对象,用于存储函数的计算结果。

  • 实现目的:减少不必要的计算,提高函数的执行效率。

function memoize(func) {
  const cache = {};

  return function(arg) {
    if (cache[arg]) {
      return cache[arg];
    } else {
      const result = func(arg);
      cache[arg] = result;
      return result;
    }
  };
}

const memoizedAdd = memoize(function(x) {
  console.log('Calculating sum...');
  return x + x;
});

console.log(memoizedAdd(5)); // 输出: Calculating sum... 10
console.log(memoizedAdd(5)); // 输出: 10 (来自缓存)

memoize 函数使用闭包内部的 cache 对象来缓存函数的计算结果,确保在相同参数的情况下能够直接返回缓存的结果,提高了函数的执行效率。

闭包可能存在的问题

1. 内存泄漏

闭包会使得函数内部的作用域保持活动状态,导致函数内部的变量无法被垃圾回收机制释放。如果闭包中包含大量数据或者引用,而且这些引用没有被释放,就可能导致内存泄漏问题。

2. 变量共享和意外修改

由于闭包中的函数可以访问外部作用域中的变量,当闭包被多次调用时,可能会导致多个闭包共享相同的外部变量,造成意外的变量修改和不可预料的行为。

3. 性能问题

使用闭包会增加内存消耗和执行时间,因为闭包需要维护外部作用域的引用。在频繁创建大量闭包的情况下,可能会对性能造成一定影响。

4. 难以调试和理解

闭包的嵌套结构可能会使代码变得复杂,尤其是当闭包嵌套多层时,会增加代码的可读性和维护难度,同时也会增加调试的复杂性。

5. 潜在的安全问题

闭包可能导致潜在的安全问题,特别是在涉及到私密数据或者敏感操作时,需要谨慎处理闭包的使用,避免数据泄露或恶意篡改。

最后

通过对闭包知识点的整理,是否对闭包更为清晰了呢?

文章转自:https://juejin.cn/post/7374265502669799458

  • 40
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值