JS高级 - 闭包

143 阅读4分钟

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures)

任何一个函数式编程语言都支持闭包

当一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数和其所对应的词法环境就一起组成了闭包

  • 自由变量 - 如果在函数内部使用了一个变量,但这个变量不是在当前作用域中定义的,而是在别的作用域中定义

    的时候,这个变量就被称之为自由变量

  • 词法环境:在ES6之前,执行上下文中对应的变量和函数是被存储在VO中的

    而自ES6开始,执行上下文对应的变量如果是使用let/const 定义的变量会被存放到词法环境(lexical environment)中

    对于使用var定义的变量会被定义到变量环境(VariableEnvironment VE)中

    而这些词法环境,VO,VE 就是形成闭包时候所对应的周围环境

词法环境由以下两部分组成:

  • 环境记录(EnvironmentRecord): 储存变量和函数声明的实际位置

  • 对外部环境的引用(Outer):当前可以访问的外部词法环境(outer的值可能为空,例如全局环境下就没有对应的outer)

在JS中,每一个函数都会存在对应的作用域链,通过作用域链,可以查找到函数的上层作用域中的变量

所以作用域链可以看成是JS实现闭包的机制

因此在广义上来说,JavaScript中的函数都是闭包

但是从狭义的角度来说:JavaScript中一个函数,只有访问了外层作用域的变量,那么它才是一个闭包

正是因为JS闭包机制的存在,才可以让我们在一个函数的内部去访问和使用函数外部的变量和方法

闭包的内存泄露

function createAdder(count) {
  function adder(num) {
    // adder函数内部使用了自由变量count
    // 所以构成了狭义上的闭包
    return count + num
  }

  // 因为adder函数被返回,即外部有引用指向adder函数
  // 所以adder函数和createAdder函数的函数对象,scope chain和VO都无法得到有效的释放
  return adder
}

var adder5 = createAdder(5)
adder5(100)

var adder8 = createAdder(8)
adder8(22)

此时代码执行完毕后,内存示意图如下:

image.png

图中所有的对象都可以通过GO对象被遍历到,也就是具有可达性

所以对于GC来说,所有的对象都是可能会被使用到的对象,没有对应的垃圾对象

但是如果此时我们不在使用adder8这个函数,

那么就意味着adder8所对应的函数对象,VO对象和adder8的scope chain对象都不需要在被使用

但是通过上图可以看出,因为在GO中有引用指向adder8所对应的函数对象,所以adder8所对应的VO,函数对象和scope chain对象都无法得到有效释放

此时长久以往,就会导致可用的内存逐渐变少,这个情形就被称之为内存泄露

所以对于闭包,我们需要手动释放对应的内存空间

// 当我们移除GO中对于adder8所对应的函数对象的引用的时候
// adder8所对应的VO,函数对象和scope chain对象就会变成垃圾对象
// 在下次GC进行回收的时候会被移除
adder8 = null

浏览器对于闭包的优化

function fun() {
  // 在规范上来说,因为产生了闭包
  // 所以fun所对应的VE对象会被保留下来
  // 因此name,age, address都会被保留下来
  // 但是在本例中,address并没有被fun所返回的那个函数所引用
  // 所以浏览器会进行相应的优化操作,也就是说
  // 浏览器所保留的fun函数的VO对象中只有name和age
  // 并不存在address
  let name = 'Klaus'
  let age = 24
  let address = 'shanghai'

  return function() {
    // 此时在控制台的console页签中
    // 可以获取到name的值和age的值
    // 但是无法获取到address的值
    // 所以fun因为闭包而保留下来的VO对象中
    // 只有name和age,并没有address
    debugger

    console.log(name, age)
  }
}

fun()() // => Klaus 24