【js基础巩固计划】深入理解闭包

努力让学习成为一种习惯,自信来源于充分的准备

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

前言

闭包 一直让人头疼的一个知识点,不太容易理解。而且使用不当还有副作用(内存泄露)。但是js代码中往往充斥着大量的闭包。因此理解闭包并合理正确的使用是很重要的。

这是js基础巩固系列文章的第二篇,旨在帮助自己巩固js相关知识,同时也希望能给大家带来些新的认识,如有疑问出入,欢迎评论区一起讨论交流

什么是闭包

深入理解作用域与作用域链一文中,我们知道了函数的词法作用域在预解析阶段就已经确认好了。在其内部属性[[Scopes]]可以体现,

如下代码:

js 复制代码
function func() {
  let age = 18
  function inner() {
    return age
  }
  console.dir(inner)
  return inner
}
func()

其中[[Scopes]]中的 Closure(func)就是一个闭包

我们来看看官方对闭包的定义

JavaScript权威指南: 闭包是指有权访问另一个函数作用域中的变量的函数

mdn: 闭包是函数本身与其词法环境的组合

按照定义,从严格意义来讲下面的代码就是形成一个闭包(当前函数作用域内使用了外层作用域的变量)

js 复制代码
let a = 1
function func() {
  return a
}

JavaScript权威指南中说道:所有js函数都是闭包

但是仔细一想,这好像和我们实践中的闭包不一样呀

我们来看看实践中比较常见的一段代码:

js 复制代码
let a = 1
function outer() {
    let outer = 'outer'
    function inner() {
        let b = 'b'
        console.log(a)
        console.log(outer)
        console.log(b)
    }
    console.dir(inner)
    return inner
}
outer()

通过上面的\[Closure]属性我们可以发现几个点:

  1. 全局作用域中声明的变量a没有出现在[[Closure]]闭包中
  2. inner函数自身作用域内的声明的变量b没有出现[[Closure]]闭包中

其实通过上面的控制台打印,我们可以发现与 mdn红宝书的定义有出入(闭包 内部只保存了外层词法环境的变量outer,全局变量a与函数本身作用域的变量b没有保存)

我们可以给闭包一个实践上的定义:(其实没必要死抠概念,重在理解)

函数可以记住并访问所在的词法作用域。当函数在当前词法作用域之外调用便产生的闭包。换句话说:闭包是内部函数引用外部函数变量的集合

闭包的使用

定时器

js 复制代码
for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i); // 6
  }, i * 1000);
}

上面这段代码如果要使输出符合我们正常的预期应该怎么解决?

第一种:直接使用支持块级作用域的关键字let,这里不是本文的主题,不多描述。具体可以看我的这篇文章:你真的理解变量提升吗

第二种:使用闭包 (严格来说上面这段代码已经形成了一个闭包,但是不符合我们的预期,需要改造)

js 复制代码
for (var i = 1; i <= 5; i++) {
  (function(i) {
    setTimeout(function timer() {
      console.log(i); // 1,2,3,4,5
    }, i * 1000);
  })(i)
}

我们来简单分析下上面这段代码

每次循环,立即执行函数都会产生一个新的作用域,定义器中的回调函数引用了外层词法作用域的变量。立即执行函数执行完成,其执行上下文被销毁,但是由于闭包 ,其参数变量i没有被销毁而是保存到了内存中。并形成了独自的作用域。每次输出的值便是当初外层函数传入的值

模拟私有变量

js 复制代码
function Fruits() {
  let fruits = []
  this.getFruits = function() {
    return fruits
  }
  this.addFruits = function (fruit) {
    fruits.push(fruit)
  }
}
const f = new Fruits()
console.log(f.getFruits()) // []
f.addFruits('apple')
console.log(f.getFruits()) // ['apple']
f.addFruits('banala')
console.log(f.getFruits()) // ['apple','banala']

上述代码中我们无法直接访问/操作其内部的变量fruits,只能通过暴露出来的getFruitsaddFruits间接操作fruits

当构造函数执行完成,执行上下文被销毁,但其内部的变量fruits由于闭包的作用被保存在了内存中

注意到闭包的一个特点:它保存在内存的数据并不是一份快照,而是对同一个闭包对象(同一个执行上下文下)Closure (foo)的引用,因此会有累加的效果(如果是多次调用函数,则会产生多个执行上下文,此时也会有多个闭包对象,互不干涉)

这里我们可以思考一个问题:

如果js没有闭包这个概念呢,我们对代码略微调整

js 复制代码
let fruits = []
function Fruits() {
  this.getFruits = function() {
    return fruits
  }
  this.addFruits = function (fruit) {
    fruits.push(fruit)
  }
}
const f = new Fruits()
console.log(f.getFruits()) // []
f.addFruits('apple')
console.log(f.getFruits()) // ['apple']
f.addFruits('banala')
console.log(f.getFruits()) // ['apple','banala']

我们将fruits放到全局作用域了。功能也是没有问题的,那为什么不采用这种污染全局作用域的方式呢。答案显而易见:如果有多个实例呢,功能瞬间会出现问题,这时候你可能已经体会到了闭包的设计是多么的精妙

内存泄露

大多数人"谈闭色变"的原因之一就是可能会造成内存泄露

内存泄露指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

我们需要明白一个事情:闭包本身与内存泄露并没有直接关系,但是闭包容易造成内存泄露

js 复制代码
function createIncrease() {
    const doms = new Array(1000).fill(0).map(item => {
      const dom = document.createElement('dom')
      dom.innerHTML = item
      return dom
    })

    function increase() {
      doms.forEach(dom => {
        dom.innerHTML = Number(dom.innerHTML) + 1
      })
    }

     return increase
}

const increase = createIncrease()
const btn = document.querySelector('button')
btn.addEventListener('click', increase)

上面这段代码中increase只调用一次就不需要了,但是doms却一直被保存在内存中,从而造成内存泄露

我们可以对上面代码进行优化

js 复制代码
  function createIncrease() {
    const doms = new Array(1000).fill(0).map(item => {
      const dom = document.createElement('dom')
      dom.innerHTML = item
      return dom
    })

    function increase() {
      doms.forEach(dom => {
        dom.innerHTML = Number(dom.innerHTML) + 1
      })
    }

    return increase
  }

  const btn = document.querySelector('button')

  function handleClick() {
    const increase = createIncrease()
    increase = null
    btn.removeEventListener('click', handleClick)
  }

  btn.addEventListener('click', handleClick, {
       once: true
  })

上述代码,按钮点击执行一次后,便会把相关函数的引用给置空。浏览器判断increase函数不可再被访问,进而进行垃圾回收,避免内存泄漏

我们再次把上面代码进行改造

js 复制代码
function createIncrease() {
    const doms = new Array(1000).fill(0).map(item => {
      const dom = document.createElement('dom')
      dom.innerHTML = item
      return dom
    })

    function increase() {
       // increase内部没有引用 doms,没有形成闭包
    }

    function bar() {
      doms
    }

    return increase
}

const btn = document.querySelector('button')
const increase = createIncrease()
btn.addEventListener('click', increase)

代码中increase内部没有引用 doms,没有形成闭包。但其中另外一个函数引用了。此时依旧会带来内存泄露

另外有个点需要注意,闭包产生了,并不一定会永远放在内存中

js 复制代码
function foo () {
    var name = 'liuxy'
    var age = 18

    function bar () {
        console.log(name)
        console.log(age)
    }
    return bar
}

foo()()

上述代码中,产生闭包的内部函数bar被调用后,其执行上下文被销毁。那么保存在bar[[Scopes]]上的Clourse(foo)对象自然也会被销毁

到这里我们给闭包内存泄漏的关系做一个总结:

  1. 闭包产生内存泄漏的原因与其他场景并没有什么不同:引用了一个不再需要使用的函数,会导致函数关联的词法环境无法被销毁,从而导致内存泄漏
  2. 当多个函数共享词法环境,会导致词法环境膨胀,会出现无法触达也无法回收的内存空间,从而导致内存泄漏

有关垃圾回收的细节后续会单独文章描述

结语

闭包无处不在,日常业务中的定时器ajax网络请求交互事件的逻辑函数缓存等,高阶的偏应用函数柯里化等都有闭包的功劳在里面。不知不觉,在我们的开发过程中,大量使用了闭包,闭包从一定角度极大的方便了我们的开发,提高了开发效率

到这里,就是本篇文章的全部内容了

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

如果你有疑问或者出入,评论区告诉我,我们一起讨论

参考文章

JavaScript深入之闭包

相关推荐
做前端的娜娜子1 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅1 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni1 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学3 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师3 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc883 小时前
微软EDGE浏览器功能学习
前端·学习·edge
Csvn4 小时前
🎯 原生 `<dialog>` 元素:终于可以扔掉一半的自定义弹窗组件了?
前端
Csvn4 小时前
🎨 CSS @layer:用「层叠优先级」终结样式打架的世纪难题
前端
濮水大叔4 小时前
不必把 Vue3 写成“麻花”:从状态碎片到对象协作,重新理解 Zova 的前端心智模型
前端·typescript·vue3·ioc·tsx·zova