最纯粹的闭包理解

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

相关推荐
莹雨潇潇2 分钟前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr10 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ2 小时前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
john_hjy3 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd3 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js