最纯粹的闭包理解

闭包是js中非常重要又难以掌握的知识点,尤其以理解了简单的闭包之后,各种框架里的语法都会对闭包大量应用。万丈高楼从地起,我们先了解一下简单的闭包吧

理解闭包

看一段简单的代码

php 复制代码
function foo () {
    var name = 'foo'
    function bar () {
        console.log('bar', name)
    }
    return bar
}
var fn = foo()
fn() // bar foo

在这个例子中,bar()显然是可以被正确执行的,但是有一个很特殊的现象,bar是定义在foo函数的作用域里的函数,但是执行的地方却不是在foo函数的作用域里,也就是说,它在自己定义的词法作用域以外的地方执行了 。foo()执行后,通常会期待foo()的整个内部作用域被摧毁,而闭包的神奇之处就是阻止了这个过程,原因是bar()在引用着foo()的作用域

拜bar()声明的位置所赐,它拥有涵盖foo()作用域的闭包,使得foo()的作用域可以一直保存。

什么叫闭包呢?bar()依然持有对该作用域的引用,这个引用就是闭包。也就是说,如果函数在定义时的词法作用域以外的地方被调用,就产生了闭包闭包使得这个函数引用的词法作用域一直存在,这就是闭包的定义。

闭包的原理

在程序中,变量存放的位置有全局空间(堆结构)和局部空间(栈结构)。

在JavaScript中,函数的变量一般都存储在局部空间,但是如果被闭包引用的变量会存储在全局空间,也正是因为如此才能在函数执行完毕之后才能使得变量被一直使用。因此在JavaScript中无论是存储在全局空间还是局部空间的变量,声明方式都是一样的,let a = 1,因此要理解闭包,避免在声明变量的时候造成内存泄露

闭包的应用

  1. 对函数类型的值进行传递,当函数在别处调用时,产生了闭包
scss 复制代码
function foo(){
  var a = 2
  function baz(){
    console.log(a);
  }
  bar(baz)
}

function bar(fn){
  fn() //妈妈快看呀,这就是闭包!
}

foo()和bar()都是定义在全局作用域下,baz()是定义在foo()的作用域下,当执行bar()时,baz()通过值传递的形式将其传到了bar的作用域里并调用,符合闭包的定义:函数在定义时的作用域以外的地方被调用,因此这里发生了闭包,读到这里,是不是觉得自己平时大多数时间都在使用闭包而不自知。

javascript 复制代码
var fn

function foo(){
  var a = 2
  function baz(){
    console.log(a);
  }
  fn = baz //将baz分配个全局变量
}

function bar(){
  fn() //妈妈快看呀,这就是闭包!
}

间接传递函数也是闭包。无论通过何种手段将内部函数传递到所在的词法作用域以外,他都会有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包

  1. 计时器

前面的代码或许有些许死板,但接下来的代码和我们的日常生活息息相关

javascript 复制代码
function wait(message){

  setTimeout(function timer(){
    console.log('message');
  },1000)
}

wait('hello,closure')

在这里,timer()作为参数,将其传递给setTimeout(),此时timer是定义在wait()函数里面,但是真实调用的地方却是在这个作用域以外,很明显,这是一个闭包,因此timer会抱有wait()作用域的闭包。

深入到引擎的内部原理中,内置的工具函数setTimeout()持有的参数函数的引用,可能叫fn或者func,在setTimeout的作用域中会调用这个函数,而这个函数在这个过程中作用域保持完整。

与setTimeout类似的,许多内置的函数例如事件监听器,Ajax请求,跨窗口通信,Web Workers或者其他的异步(或者同步)任务中,只要使用了回调函数,就是在使用闭包,因为这里都涉及到将函数作为参数传递到另一个作用域中

3.循环和闭包

css 复制代码
for(var i = 0; i <= 5; i++){
  setTimeout(() => {
    console.log(i);
  },i。*1000)
}

这段代码会以每秒一次的频率输出五次6,这是因为setTimeout作为宏任务,会在for循环结束时js引擎才会去执行,然后通过rhs查找i,找到的是全局的i,此时的i是6。没有出现每隔1秒依次输出1,2,3,4,5的原因正是因为没有每次循环时没有形成闭包,没有记住之前的作用域。

scss 复制代码
for(var i = 0; i <= 5; i++){
  function fn(){
    let j = i
    setTimeout(() => {
      console.log(j);
    },i*100)
  }
  fn()
}

只需要通过闭包创建一个会被记住的作用域即可。

同样我们也知道,这样写的代码也是有一样的效果。

css 复制代码
for(let i = 0; i <= 5; i++){
  setTimeout(() => {
    console.log(i);
  },i*100)
}

let本质上是将一个块转化成一个可以被关闭的作用域,也是利用了闭包的性质

总结

闭包的本质就是函数在定义的作用域以外的地方被调用了,这个函数会引用着之前的作用域。也正是因为一直被引用着,会导致没有被垃圾清理器清除导致内存越占越大,因此需要将这个函数赋值为null,才能避免内存泄露的问题

本文是参考《你不知道的JavaScript》,相比于网上的其他文章,个人感觉并并没有写得太多太复杂,仅仅解释了最简单的闭包应用,个人认为有一种回归最原始的闭包的定义,让每一个用过js的人都可以理解,希望可以帮助你开始理解闭包,理解闭包的妙用。

相关推荐
訾博ZiBo6 分钟前
VibeCoding 时代来临:如何打造让 AI 秒懂、秒改、秒验证的“AI 友好型”技术栈?
前端·后端
excel13 分钟前
彻底理解缓冲区:从概念、背压到可运行的 Fetch 流式示例
前端
小蜜蜂嗡嗡3 小时前
【flutter对屏幕底部有手势区域(如:一条横杠)导致出现重叠遮挡】
前端·javascript·flutter
伍哥的传说4 小时前
Vue 3 useModel vs defineModel:选择正确的双向绑定方案
前端·javascript·vue.js·definemodel对比·usemodel教程·vue3.4新特性·vue双向绑定
胡gh9 小时前
页面卡成PPT?重排重绘惹的祸!依旧性能优化
前端·javascript·面试
胡gh9 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
言兴9 小时前
# 深度解析 ECharts:从零到一构建企业级数据可视化看板
前端·javascript·面试
山有木兮木有枝_9 小时前
TailWind CSS
前端·css·postcss
烛阴10 小时前
TypeScript 的“读心术”:让类型在代码中“流动”起来
前端·javascript·typescript
杨荧10 小时前
基于Python的农作物病虫害防治网站 Python+Django+Vue.js
大数据·前端·vue.js·爬虫·python