(三) js闭包

时间:2021-07-26
本文章向大家介绍(三) js闭包,主要包括(三) js闭包使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. 案例引入

先看一个简单的需求:

  • 我们想要实现一个简单的累加器, 每调用一次累加函数, 变量就加 1

这真的是一个简单的需求, 我们可以立马写出来

var counter = 0
function addCounter(){
  counter++
  console.log(counter);
}
addCounter()				// 1
addCounter()				// 2
addCounter()				// 3

但是你有没有想过: 为什么 counter 可以一直被累加 ?

这里就牵扯到了js中的 垃圾回收机制

所谓的垃圾回收, 就是确定哪个变量不再使用, 然后释放它占用的内存, 这是一个周期性的检测

但是对于全局变量而言, 垃圾回收机制并不知道它什么时候不再使用, 因此也就无法释放其占用的内存, 全局变量也就会一直被保存在内存当中, 这也就是我们可以一直使用它的原因

其实从执行上下文的角度来看, 全局变量的声明是在全局上下文中进行的, 被一直保存在GO中, 而全局上下文是在我们关闭浏览器时才会被销毁, 因此其他函数就可以通过其保存的上下文环境 (也就是作用域链), 到GO中引用counter

这样, 第一个问题我们就解决了

好, 我们再来思考另外一个问题, 如果另外一个函数也用到了 counter, 或者另外一个开发者声明了一个同样的全局变量 counter,那么这势必会影响上面累加器的运作, 因为 counter 的值被其他函数 / 变量声明 改变了, 这显然是我们所不希望的

那既然不想counter被其他因素所改变, 那我们把counter作为累加器的内部变量不就行了么, 这样只有函数自己能用, 行吗? 显然不行

因为函数在执行完毕后, 会销毁其所在的上下文环境, 保存在AO中的变量会被垃圾回收机制释放, 对于无法一直保存在内存中的变量, 我们显然无法一直对他进行操作

那有没有办法可以解决这个需求: 我们既想把 counter 声明为局部变量, 又让他一直保留在内存中呢 ?

答案是可以的, 这就是我们需要闭包的原因

2. 什么是闭包

个人理解:

  • 我们知道: 执行上下文是一个运行时环境, 当函数执行完毕, 函数上下文会被释放, 局部变量自然也就无法一直保留

  • 而闭包的存在, 就是帮助我们把这些会被释放的上下文保存到内存中,使其一直存在

MDN中给出闭包的如下定义:

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。

示例

function fn() {
  var count = 0
  function addCount() {
    count++
  }
  addCount()
}

fn()

上面有没有形成闭包呢? 我们根据MDN的定义来分析一下:

  • addCount 是不是一个函数? 是
  • addCount 对其周围状态有没有引用 ? 有, 引用了所处上下文环境中的 count

既然形成了闭包, 那能不能完成我们上面的需求呢: **我们既想把 counter 声明为局部变量, 又让他一直保留在内存中 **

我们来接着调用addCount函数, 看三次调用以后, count的值是不是3

注意看 watch中count的变化

通过演示我们发现, count 并没有一直被保留在内存中, 而是每次调用都被初始化为了0

这显然无法完成我们的需求


ECMAScript中, 给出如下定义:

  • 从理论角度:所有的函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  • 从实践角度:以下函数才算是闭包:

    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

根据ECMAScript给出的定义, 我们再来对比MDN给出的定义, 我们发现, MDN中对闭包的定义其实就是ECMAScript中的闭包的理论角度,

但这对我们的需求并没有实际的作用, 那我们再来看 从实际角度出发给出的定义

示例

function fn() {
  var count = 0
  function addCount() {
    count++
  }
  return addCount
}

var add = fn()
add()
add()
add()

注意: 与上面代码不同的是, 我们这次将addCount函数作为返回值传递到外部

注意此时watch中count的变化

我们可以清晰的看到, 内部变量count, 在函数外部被一直引用, 而且实现了值的累加 !

也就是说, count被一直保留在了内存之中, 这也真正实现了我们上面所一直所提的需求:

  • 我们既想把 counter 声明为局部变量, 又让他一直保留在内存中

其实说了这么多, 可能还是比较抽象, 我们先回顾以下闭包的理解:

  • 我们知道: 执行上下文是一个运行时环境, 当函数执行完毕, 函数上下文会被释放, 局部变量自然也就无法一直保留

  • 而闭包的存在, 就是帮助我们把这些会被释放的上下文保存到内存中,使其一直存在

我们举个例子:

​ 假设我家院子 (内存)里有一个大水缸 (不能释放的内存), 水缸里面有满满的水 (全局变量), 这个水过路口渴的人 (其他函数等)都可以取来喝,但是不同的人都来喝, 不就把我家的水给 污染 了么, 况且这是我家, 你们怎么能想喝就喝呢, 于是我想出了这样一个办法:

​ 我先把水(此时变成局部变量)都移到纸箱(外层函数)里, 但是纸箱存不住水 (函数被释放)呀, 于是我又马上把这些水都灌进水瓶 (内层函数)里, 然后把这些水瓶放在纸箱里 (函数嵌套), 这样我们的水得以保存, 但是别人也想喝怎么办, 那就按照我的说明去纸箱里拿水瓶 (暴露给外部的引用, 到此形成闭包), 并且你只能通过暴露出的水瓶来喝水

​ 后来家家装了自来水, 大家也都不需要来我家喝水了, 于是我就把所有的水瓶丢掉(清除引用, 释放闭包的内存),


综上:

怎么理解闭包:

  • 我们知道: 执行上下文是一个运行时环境, 当函数执行完毕, 函数上下文会被释放, 局部变量自然也就无法一直保留

  • 而闭包的存在, 就是帮助我们把这些会被释放的上下文保存到内存中,使其一直存在

什么才算闭包:

从实践角度:以下函数才算是闭包:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量

3. 闭包的应用

1. 访问函数内部的变量, 也就是上面一直提的需求

function fn() {
  var count = 0
  function addCount() {
    count++
  }
  return addCount
}

var add = fn()
add()

2. 定时器传值, 本质与上面一致

function func(param) {
  return function () {
    alert(param)
  }
}

var f1 = func(1)

setTimeout(f1, 1000)

3. 利用闭包判断数据类型

function isType(type) {
  return function (target) {
    return `[object ${type}]` === Object.prototype.toString.call(target)
  }
}

const isArray = isType('Array')
console.log(isArray([1, 2, 3]));				// true
console.log(isArray({}));	
仅记录自己的学习总结,如有错误,还请评论指正~

原文地址:https://www.cnblogs.com/zhimao/p/15061125.html