掌握闭包,夯实基本功

简介: 闭包在程序中无处不在,通俗来讲闭包就是一个函数对其周围状态的引用并捆绑在一起的一种组合,你也可以理解成一个函数被引用包围或者是一个内部函数能够访问外部函数的作用域

闭包在程序中无处不在,通俗来讲闭包就是一个函数对其周围状态的引用并捆绑在一起的一种组合,你也可以理解成一个函数被引用包围或者是一个内部函数能够访问外部函数的作用域


闭包是面试经常考的,也是了解一个程序员基础知识一个重要点,本篇是笔着对于闭包的理解,希望在实际项目中有所思考和帮助。


正文开始...


闭包是什么


我们可以从以下几点来理解


  1. 闭包是一个函数对其周围状态引用并捆绑在一起的一种组合


  1. 一个函数引用包围


  1. 一个内部函数能访问外部函数作用域


我们来看一张图理解下上面三句话

56872fbe4829693461b33d8ea3224cb7.png

对应代码如下

function A() {
    var name = 'Maic',age = 0;
    function B() {
        console.log(name, age);
    }
}
A();
// console.log(name) name is not defined

我们注意到在A函数中,我们创建了两个内部的私有变量nameage,并且我们在A函数中创建一个内部函数B,此时在B函数中,我们会发现在B内部可以访问它周围状态(变量),也就意味着在B函数内部可以访问外部函数的作用域。


至此你会发现,闭包就是在B函数一创建,并且有对周围的状态有引用,那么此时闭包就出现了,也就是说,闭包就是一座桥梁,能让B函数内部能突破自身作用域访问外部的变量。


不知道你有没有发现,我在A内部定义的变量,我在外部并不能访问,也就是说相对A的外部,A内部所有的变量都是私有的,在A定义的变量,相对于B中,又可以访问。因为B函数能访问A中的变量,也正是依靠闭包这座桥梁。


闭包的特性


1.创建私有变量


2.延长变量的生命周期


我们知道闭包会造成内存泄露,本质上就是创建的变量一直在引用内存中,当一个普通函数被调用结束时,函数内部创建的变量就会被销毁。


但是闭包会保存其变量的引用,即便外部执行上下文被销毁,但是闭包内创建的词法环境依然还在,我们看下面代码具体理解下。

function A() {
    var name = 'Maic',age = 0;
    function B() {
        age++;
        console.log(name, age);
    }
    return B
}
var b1 = A();
b1(); // 1
b1(); // 2
b1(); // 3

A中返回了函数B,实际上就是返回了一个函数,当我们我们用var b1 = A()申明一个变量时,实际上,这里内部B还没有执行,但是在执行A()方法时,返回的是一个函数,所以我们继续执行b1(),我们尝试调用三次,我们会发现打印出来的值是1,2,3,这就说明,闭包延长了变量的生命周期,因为第三次与第二次打印出来的值就是同一个值的引用。具体一张图可以可以理解下

34e3c25e736eff1af67aeff4e4958e44.png

当我们用var b1 = A()时,实际上,我用蓝色的方框已经标注起来了,在b1内部我们可以看到,每执行b1,实际就是执行的红色区域的函数,也就是A内部定义的函数B,但是每次调用b1,我们看到都有保留age的引用,所以你看到age依次就是1,2,3,所以也就证实了闭包能延长变量的生命周期,并且闭包创建的私有变量可以减少全局变量的使用。


通常我们知道尽量少创建全局变量,因为我们不知道这个全局变量什么时候使用,只有在被使用的时候才会被释放。闭包也是解决了全局变量命名冲突的问题,因为创建的私有变量,没法在外部访问,这样也就减少了变量名污染的问题。


等等,还有一个问题,如果我把上面的代码改成下面呢?

function A () {
  var name = 'Maic',age = 0;
  function B() {
      age++;
      console.log(name, age);
  }
  return B
}
A()(); // 1
A()(); // 1
// var b1 = A();
// b1();
// b1();
// b1();
// console.log(name)

你会发现,我两次打印的都是同一个1,这是为什么呢?你有没有发现之前我们是用var b1 = A()申明的一个变量,实际上这句代码就是js新开辟暂存了一块空间,因为A内部返回的是一个函数,当我每次调用b1时,实际上是调用返回的那个函数,因为函数内部存在闭包的引用,所以一直就1,2,3,但是我这里我使用的是A()(),我们发现每次都是1,说明当我第二次调用时内部的age已经重新定义了一遍,而并没有引用上一次的值,这就说明,在A()立即调用时,闭包内部引用的变量已经被释放。由于闭包也会有缺陷,创建太多的闭包会造成消耗内存严重,影响网页性能。


应用场景


  1. 柯里化函数


  1. 回调函数


  1. 计数器延迟调用(防抖与节流)


  • 柯里化函数


实际上就是把一个函数的多个参数拆分成多个函数调用,主要目的是避免平繁调用具有多个相同参数函数,又可以复用相同函数,具体可以用下面代码理解下

// 未柯里化之前
function sum(a,b,c) {
  return a+b+c 
}
sum(1,2,3)

函数柯里化后

function sum(a) {
  return function(b) {
      return function(c) {
        return a+b+c
      }
  }
}
const a = sum(1);
const b = a(2);
const c = b(3);
console.log(c) // 6 or sum(1)(2)(3)

上面🌰好像还是不太明显,在具体业务中,你可能会写出这样的代码

// 根据正则规则校验某个字段
function regKey(reg, val) {
    return reg.test(val)
}
var isPhone = regKey(/^1[3,5,7,8,9]\d{9}$/, 13754123124);
const isNumber = regKey(/\d/, 'test');

改成函数柯里化后

function regKey(reg) {
  return (val) => {
     return reg.test(val) 
  }
}
const checkPhone = regKey(/^1[3,5,7,8,9]\d{9}$/);
const checkNum = regKey(/\d/);
const isPhone = checkPhone(13754123124) // true
const isNumber = checkNum(123); // true

我们发现改完后,貌似柯里化后,代码反而变多了,但是代码的可读性以及拓展性比以前更友好,这点因特殊业务功能而定,也不是非要把用柯里化函数去处理所有的业务,具体因情况而定,这里只是举了个简单的例子。


  • 回调函数


回调函数在业务中使用的太多了,具体可以看下下面这个简单的例子,写一段为伪代码感受一下

const request = (params) => {
  const response = {
      code: 0, 
      success: '成功',
      data: []
  };
  return (callback) => {
      callback(response);
  }
}
const queryList = () => {
    const getData = request({pageIndex:1, pageSize: 10});
    getData((res) => {
      console.log(res) // {code: 0, success: '成功', data: []}
    })
}
  • 计数器


这个就非常典型了,比如说一个循环里面

for (var i=0;i<10;i++) {
    (function() {
      var j = i;
      setTimeout(() => {
        console.log(j)
      }, i* 1000)
    })()
  }
  • 函数节流


频繁触发事件,在指定一段时间内调用函数

// 模拟数据请求伪代码
  const fetchList = () => {}
  let flag = false;
   window.addEventListener('scroll', () => {
      if (flag) {
        return;
      }
      flag = true;
      setTimeout(() => {
        flag = false;
        fetchList();
      }, 500)
  })
  • 函数防抖


利用定时器做缓冲器,当第二次调用时,清除上一次的定时器,在指定时间内重新调用函数

// 模拟数据请求伪代码
const fetchList = () => {}
const inputDom = document.getElementById('input');
let timer = null;
inputDom.oninput = function() {
    if (!timer) {
        clearTimeout(timer);
        timer = setTimeout(() => {
          fetchList();
        }, 500)
    }
}

以上案例都有用到闭包,闭包的身影无处不在,只是我们用的时候,我们并没有发现。


总结


  • `闭包`[1]的概念,闭包是一个函数对其周围状态引用捆绑在一起的一种组合,或者是一个函数引用包围,或者是一个内部函数能访问外部函数的作用域


  • 闭包的特性,创建私有变量延长变量的生命周期


  • 闭包的应用场景,柯里化函数回调函数定时器节流/防抖


  • 本文示例code example[2]
相关文章
|
机器学习/深度学习 存储 监控
「Arm Arch」 初识 Arm(上)
「Arm Arch」 初识 Arm(上)
1444 1
|
12月前
【HarmonyOS Next之旅】ArkTS语法(四) -> 使用限制与扩展
本文介绍了ArkTS语言在生成器函数中的使用限制、变量的双向绑定以及自定义组件成员变量初始化的方式与约束。生成器函数中表达式的使用场景受限,且不能改变状态变量或包含局部变量。事件处理函数不受这些限制。双向绑定通过$$实现,支持基础类型及特定装饰器变量,变更时仅渲染当前组件以提升效率。成员变量初始化支持本地和构造参数两种方式,不同装饰器类型的变量有不同的初始化规则和约束,需根据具体需求选择合适的初始化方法。
376 21
|
存储 分布式计算 物联网
Apache IoTDB进行IoT相关开发实践
IoTDB是专为物联网(IoT)设计的开源时间序列数据库,提供数据收集、存储、管理和分析。它支持高效的数据写入、查询,适用于处理大规模物联网数据,包括流数据、时间序列等。IoTDB采用轻量级架构,可与Hadoop和Spark集成,支持多种存储策略,确保数据安全和高可用性。此外,它还具有InfluxDB协议适配器,允许无缝迁移和兼容InfluxDB的API和查询语法,简化物联网项目的数据管理。随着物联网设备数量的快速增长,选择适合的数据库如IoTDB对于数据管理和分析至关重要。
826 12
|
存储 安全 数据管理
区块链在医疗数据管理中的应用:安全与隐私的新纪元
区块链在医疗数据管理中的应用:安全与隐私的新纪元
954 16
|
缓存 Linux 数据库
CentOS 8中 更新或下载时报错:为仓库 ‘appstream‘ 下载元数据失败 : Cannot prepare internal
通过以上步骤,您可以有效地解决 CentOS 8 中“为仓库 ‘appstream’ 下载元数据失败 : Cannot prepare internal”问题。关键在于检查网络连接、更新和切换仓库配置、清理缓存、重建 RPM 数据库以及在必要时临时禁用有问题的仓库。通过这些方法,可以确保系统能够正常进行软件包的更新和下载操作。
3968 20
蓝桥杯嵌入式第十一届(第一场)省赛
蓝桥杯嵌入式第十一届(第一场)省赛
538 1
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的库存管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的库存管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
213 0
SAP 彻底刪除物料主数据操作
SAP彻底刪除物料主数据操作   工业标准版操作步骤: 1、先将要删除的物料标记删除(MM06) 2、运行程式MMREO001选择要删除的物料 3、运行程式MREO050 备份物料 4、运行程式REO110 删除物料也可以使用TCODE:SARA去删除物料,操作步骤基本同上。
2721 0
|
存储 算法 NoSQL
Pandas 2.2 中文官方教程和指南(二十四)(1)
Pandas 2.2 中文官方教程和指南(二十四)
134 0
|
SQL 关系型数据库 MySQL
MySQL防止重复插入(数据库层where方法解决)
MySQL防止重复插入(数据库层where方法解决)
121 0