最纯粹的闭包理解

闭包是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的人都可以理解,希望可以帮助你开始理解闭包,理解闭包的妙用。

相关推荐
Martin -Tang3 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发4 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html