闭包经典面试题 与 理解闭包

180 阅读2分钟

闭包学了好多次, 始终很迷

今天我觉得我理解了一些,从例子开始

问:输出什么

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i)
    }, 1000)
}

答案

当然是 5 个 5

记得初学js的我,心中的答案是 0,1,2,3,4

原因

首先 变量i是通过var定义的,es6之前只有函数作用域,通过var定义的变量会被提升到作用域的顶端,我理解就是提升到函数的第一行, 就像下面这样

function() {
      console.log(ui)
      var ui = 24
}

等价于

function() {
      var ui = 24
      console.log(ui)
}

回到主题,setTimeout 的执行机制是: 当遇到 setTimeout 函数,就会在指定时间之后(也就是第二个参数)把回调函数放入任务队列,等到主线程执行栈为空的时候,再依次执行任务队列里的任务,

for 循环执行完毕 i 已经变成了 5, 因此当执行到任务队列中的任务的时候,i就是5,所以打印出 5 个 5

如何打印 0,1,2,3,4

最简单的方式

最简单的方式当然是 把 var 改成 let 了,由于是块级作用域,所以在每次循环的时候, console.log(i) 中的i保持的对 let i 的引用,因此它暂时不会被垃圾回收机制处理,就能在执行到任务队列的时候沿着执行上下文找到i

关于垃圾回收机制暂时不是很懂,只是知道 1. 如果变量被其他变量引用,那么就会一直保留在内存空间中 2. 如果函数执行完毕且函数内部的变量没有被函数外引用,那么函数内的变量会被垃圾回收处理

闭包方式

for (var i = 0; i < 5; i++) {
    (function(i) {
        setTimeout(function() {
            console.log(i)
        }, 1000)
    })(i)
}

是不是发现i有点多,实际上是这样的,

我们在for循环内部写了一个立即执行函数

还记得刚才提到的函数作用域吗, 通过立即执行函数我们创建里一个新的作用域, 当 i 传递到函数内部的时候,console.log(i) 会保持着对函数形参的引用, 所以函数形参 i,不会被立刻垃圾回收,而是被保留在了当前作用域下

因此当执行到任务队列的时候,会打印出 0,1,2,3,4, 这时才进入垃圾回收机制环节

可是这和闭包有什么关系???

通过打断点,可以看到 产生了 闭包 Closure

因为我们在立即执行函数作用域下,访问了其他作用域的变量,那么当读取到这个变量的时候,就会产生闭包

重新理解闭包

在红宝书第四版 第10.14章节提到

闭包是有权限访问其他函数作用域内的变量的一个函数

这里涉及作用域与作用域链内存回收机制 等知识点

当函数执行完毕, 函数内的活动对象(变量)会被销毁, 但是由于作用于链, 它可以访问上一级作用域的变量, 因此即使上一级的函数执行完毕, 作用域也不会随之销毁, 这时子函数-也就是闭包, 就拥有了访问父级函数作用域的权限, 即使父级函数执行完毕

根据词法作用域的规则,内部函数总是可以访问外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用着外部函数的变量依然保存与内存中,这些变量的集合称为闭包。

闭包的使用场景

  • 事件绑定的回调方法
  • setTimeout 的延迟回调
  • 调用函数, 内部返回一个函数

持续学习闭包中......