闭包

定义

闭包就是函数在执行时对其定义时的词法作用域的引用。作用域链和垃圾回收机制二者相结合产生了闭包这个概念,使得函数无论在何处执行都可以访问其定义处的词法作用域内的变量。

作用

  • 在函数外部能够访问到函数内部的变量
  • 延长变量的生命周期

显式触发条件

为啥要说显式的触发,因为一般的隐式闭包看起来好像没啥用,也就很容易被忽略:

function foo() {
  let a = 123
  
  function bar() { // 实际上 bar 函数此时已经具备闭包
      console.log(a)
  }
  
  bar() // 但 bar 函数执行的地方却位于定义时的词法作用域内,闭包与当前词法作用域实际上是一致的
}

foo()

上述例子由于 bar 函数在执行的时候位于 foo 函数之内,也就是其定义时的词法作用域之内。 闭包的范围就刚好与当前词法作用域是一致的,所以此时闭包的作用可以忽略不计。

因此,要想发挥闭包的作用需要两个条件:

  • 该函数要在 其定义时的词法作用域之外 执行
  • 该函数作用域内使用了其定义时的词法作用域的其他变量(自身作用域外的变量

垃圾收集机制

js 中最常用的垃圾收集方法就是 标记清除

TIP

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 ——《JavaScript 高级程序设计》

也就是说,一个变量在 离开其声明的作用域时且没有被引用时 会被垃圾收集器标记,然后被回收清除内存无法被访问。 然而,由于闭包引用了其定义时的词法作用域,所以即便 在离开作用域后该词法作用域内的变量仍然属于被引用的状态 , 因此这些变量都 没有被回收 ,也就能够通过闭包进行访问!

表现形式

明白了本质之后,我们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?

返回函数

function f1() {
  var a = 2
  function f2() {
    console.log(a)  // 2
  }
  return f2
}

var x = f1()
x() 

函数参数

var a = 1

function foo(){
  var a = 2
  function baz() {
    console.log(a)  // 2
  }
  bar(baz)
}

function bar(fn) {
  fn()
}

foo()

回调函数

该类实质上是函数参数一类下的一些特殊场景集合。 在定时器、事件监听、跨窗口通信、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

// 定时器
setTimeout(function timeHandler() {
  console.log('111')
}, 100)

// 事件监听
$('#app').click(function() {
  console.log('DOM Listener')
})

立即执行函数表达式

var a = 2

(function fun() {
  console.log(a)  // 2
})()

参考

Last Updated:
Contributors: Vsnoy