高阶函数:闭包

什么是闭包

要解释闭包,可以从广义和狭义上去理解。

广义上来讲,所有的函数就是闭包。
狭义上来讲,必须要同时满足 2 个条件:

  • 一个函数中要嵌套一个内部函数,并且内部函数要访问外部函数的变量

  • 内部函数要被外部引用
    下面我们来举个例子:

function example(){
      var say = "hello word"
      return function(){
          console.log(say)
   	  }
}
let doSomeSth = example()
doSomeSth (); // hello word
doSomeSth (); // hello word

在这个 example 函数中,返回了一个函数,并且在这个内部函数中访问了 say 这个局部变量。调用 example 函数并将结果赋给 doSomeSth 变量,这个 doSomeSth 指向了 example 函数中的内部函数,然后调用它,最终输出 say的值hello word。

按照对函数的理解,这个 say变量应该当 example函数调用完后就销毁了,后续为什么还能通过调用 doSomeSth 方法访问到这个变量呢?

这就是因为闭包起了作用。返回的内部函数和它外部的变量 say 实际上就是一个闭包。

闭包的实质,就是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使离开了创造它的环境也不例外,这里的say就是自由变量。

自由变量可以理解成跨作用域的变量,比如子作用域访问父作用域的变量。

总结一下:变量 say 属于 example 函数的局部变量,并被内部函数使用,同时该函数也被返回出去。在外部,通过调用 example得到了内部函数的引用,后面多次调用这个内部函数,仍然能够访问到 say 变量。这样 say 作为自由变量被内部函数引用,即使创造它们的函数 example 执行完了,变量 say 依然存在,因此,这就是闭包。

为什么要使用闭包

举个简单的例子:

function example(){
    var say = "hello word"
    console.log(say);
}
example(); // hello word
console.log(say); // 报错

在这个例子中有一个名为 example 的函数,对它进行调用后。JavaScript 引擎会创建一个 example 函数的执行上下文,在其中声明 say 变量并赋值。

当该方法执行完后,上下文被销毁,say 变量也会跟着消失。这是因为 say 变量属于 example 函数的局部变量,它作用于 example 函数中,会随着 example 的执行上下文创建而创建,销毁而销毁。

这个例子和上面的那个例子形成了对比,继而也说明了,闭包可以将一些函数的局部变量保存下来,供之后的代码使用,为什么要使用这样的方法而不直接使用全局变量声明呢,这就涉及到全局命名变量的污染问题。

闭包的原理

那么闭包为什么会有这样的不符合一般函数知识的原理呢?这里需要从两个角度来认识闭包的原理
1.函数内部为什么可以访问外部函数的变量?

这个问题很好回答,在函数创建的时候,在其内部会产生一个 scope 属性,该属性指向创建该函数的执行上下文中的作用域链对象。作用域链对象包含了该上下文中的 VO/AO 对象,还有 scope 对象,当内部函数中找不到对应的变量,它就会到 scope 指向的对象中找。该对象保存着外部上下文中的作用域链对象,从该作用域链中就能找到对应的变量。这就是为什么函数内部可以访问到外部函数变量的原因。

2.为什么当外部函数的上下文执行完以后,其中的局部变量还是能通过闭包访问到呢?应该销毁了,找不到了才对啊。

其实函数在执行上下文时, scope 属性在函数创建的时候就已经确定下来了,所以即使外部函数的上下文结束了,但内部的函数只要不销毁(被外部引用了,就不会销毁),它当中的 scope 就会一直引用着刚才上下文的作用域链对象,那么包含在作用域链中的变量也就可以一直被访问到。

所以,所有函数都是拥有 scope 属性的,换一句话就是文章开头提到的广义定义,所有的函数就是闭包

闭包的优缺点

闭包的优点,主要有以下 2 点:

  • 通过闭包可以让外部环境访问到函数内部的局部变量。

  • 通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁。

举个例子:

不使用闭包:

let count = 0; // 全局计数器
let compute = function () { // 将计数器加1
    count++;
    console.log(count);
}
for (let i = 0; i < 100; i++) {
    compute(); // 循环 100 次
}

使用闭包:

var compute = function () {
    var count = 0; // 局部变量
    return function () {
        count++; // 内部函数访问外部变量
        console.log(count);
    }
}
var func = compute(); // 引用了内部函数,形成闭包
for (var i = 0; i < 100; i++) {
    func();
}

在这个例子中就不再使用全局变量,其中 count 这个局部变量依然可以被保存下来。

闭包的缺点:
其实闭包本身并没有什么明显的缺点,我们选择闭包的一部分原因是我们想主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量。把这些变量放在闭包中和放在全局作用域中,对内存方面的影响是一样的,局部变量本来应该在函数退出时被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。当然也不排除变量间的相互循环引用,导致垃圾处理机制无法及时回收这些变量,我们只需要把循环引用中的变量设为 null 即可。

本文章取自本人JS语言导师谢老师的学习总结,同时也感谢谢老师对我的谆谆教诲,感谢他带我走上前端这条道路,并让我为之不断向前

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
闭包在编程中有很多应用场景,以下是一些常见的例子: 1. 数据封装和私有变量:闭包可以用来创建私有变量,从而实现数据封装和保护。这在模块化编程和面向对象编程中非常有用。 2. 函数工厂:闭包可以用来创建一系列相关的函数,这些函数共享相同的外部变量。这在创建类似于Python中的装饰器或JavaScript中的高阶函数时非常有用。 3. 延迟执行和计时器:闭包可以用来实现延迟执行和定时器功能。例如,在JavaScript中,setTimeout和setInterval函数使用闭包来实现延迟执行和定时器功能。 4. 记忆化(Memoization):闭包可以用来实现记忆化,即缓存函数的计算结果,以便在后续调用中重用。这可以提高函数的性能,特别是在处理计算密集型任务时。 5. 事件处理和回调函数:在JavaScript等事件驱动的编程环境中,闭包常用于实现事件处理和回调函数。闭包可以捕获事件处理函数的上下文,使得事件处理函数可以访问其所需的外部变量。 6. 部分应用(Partial Application)和柯里化(Currying):闭包可以用来实现部分应用和柯里化,这是一种将多参数函数转换为一系列单参数函数的技术。这可以简化函数调用,使得代码更加简洁和可读。 7. 实现迭代器和生成器:在某些编程语言中,例如Python和JavaScript,闭包可以用来实现迭代器和生成器,这是一种用于遍历数据结构的高效方法。
07-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值