一文读懂js闭包

你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特
《人生果实》经典语录

什么是闭包

MDN 对闭包的定义为:
闭包是指那些能够访问自由变量的函数

那什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

由此,我们可以看出闭包共有两部分组成
闭包 = 函数 + 函数能够访问的自由变量

另一种说法如下

js的作用域分两种全局和局部,基于作用域链相关知识

在js作用域中访问变量的权利由内向外,内部作用域可以获得当前作用域下的变量,也可以访问包含当前作用域的外层作用域下的变量

反之则不能,也就是说在外层作用域下无法访问内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,我们想在一个函数内部也能访问另一个函数内部的变量该怎么办呢?

闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数

闭包有3个特性:

  • 函数嵌套函数
  • 函数内部可以引用函数外部的参数和变量
  • 参数和变量不会被垃圾回收机制回收

闭包的两种主要形式

函数作为返回值

function a() {
      var name = 'hong';
      return function() {
        return name
      }
    }
    var b = a()
    b() // "hong"

在这里插入图片描述

在这段代码中,a()中的返回值是一个匿名函数,这个函数在a()作用域内部,所以它可以获取a()作用域下变量name的值,将这个值作为返回值赋给全局作用域下的变量b,实现了在全局变量下获取到局部变量中的变量值

再来看一个闭包的经典例子

    function fn() {
      var num = 3
      return function() {
        var n = 0;
        console.log(++n)
        console.log(++num)
      }
    }
      var fn1 = fn()
      fn1() // 1 4
      fn1() // 1 5

一般情况下,在函数fn执行完后,函数作用域里的变量会被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,这相当于fn1=function(){var n = 0 ... },并且匿名函数内部引用着fn函数里的变量num,所以变量num无法被销毁

变量n是每次被调用时会重新创建,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后剩下孤零零的num,这里产生了内存消耗的问题

再来看一个经典例子-定时器与闭包

写一个for循环,让它按顺序打印出当前循环次数

      for (var i = 0; i < 5; i++) {
        setTimeout(() => {
          console.log(i)
        }, 100)
      }
      console.log(i) // 5
      // for循环会打印五次5

在这里插入图片描述
按照预期它应该依次输出0 1 2 3 4,而结果它输出了五次5,这是为什么呢?

由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经编程5,所以打印出来五个5,

我们为了实现预期结果应该怎么改这段代码?(ps:如果把for循环里面的var变成let,也能实现预期结果

for (var i = 0; i < 5; i++) {
        (function(i) {
          setTimeout(() => {
            console.log(i)
          }, 100)
        })(i)
      }
      console.log(i) // 5
      // for循环依次打印 0 1 2 3 4

引入闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递,100毫秒后同时打印出0 1 2 3 4
在这里插入图片描述

      for (let i = 0; i < 5; i++) {
        setTimeout(() => {
          console.log(i)
        }, 100)
      }
      console.log(i) // Uncaught ReferenceError: i is not defined
      // for循环依次打印 0 1 2 3 4

在这里插入图片描述
在ES5当中使用for循环都是采用var,而在ES6中都是采用的let,并且我们更推荐于let,

在js语言中,if和for是没有作用域,只有function才拥有,var是全局变量,let是块级作用域,const是常量(不可改变的变量称为常量);可以把let看做成更完美的var

es5中for循环使用var定义的,就相当于定义了一个全局的i,每次循环i++都可以进行改变i的值,

var进行变量提升和代码预解析后,var是在全局最前面的,es5的解决方案是采用闭包;

函数具有块级作用域,let具有块级作用域的,每次循环执行都相当于新创建了块级作用域

// 那如果我们想实现每隔100毫秒分别依次输出数字,又该怎么改呢?
for (var i = 1; i < 6; i++) {
        (function(i) {
          setTimeout(() => {
            console.log(i)
          }, i * 100)
        })(i)
      }
      console.log(i) // 6
      // for循环依次打印  1 2 3 4 5

在这段代码中,相当于同时启动5个定时器,i*100是为5个定时器分别设置了不同的时间,同时启动,但是执行时间不同,每个定时器间隔都是100毫秒,实现了每隔100毫秒就执行一次打印的效果。

闭包作为参数传递

 var num = 15
 var fn1 = function(x) {
   if (x > num) {
     console.log(x)
   }
 }
 void function(fn2) {
   var num = 100;
   fn2(30)
 }(fn1)
 // 30

在这段代码中,函数fn1作为参数传入立即执行函数中,在执行到fn2(30)的时候,30作为参数传入fn1中,这时候if(x>num)中的num取的并不是立即执行函数中的num,而是取创建函数的作用域中的num,这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值15,即30>15,打印30
在这里插入图片描述

闭包面试题

// 分别打印出什么
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

闭包的利弊

好的地方

  • 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
  • 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
  • 匿名自执行函数可以减少内存消耗

不好的地方

  • 其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动赋值为null

  • 其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响

推荐阅读

连点成线
Vue源码学习完整目录


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值