# 闭包

# 定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

详细解释 (opens new window)

# 题解

var data = [];

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

data[0]();
data[1]();
data[2]();
1
2
3
4
5
6
7
8
9
10
11

答案是都是 3,让我们分析一下原因:

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: * data: [...],
        i: 3
    }
}
1
2
3
4
5

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}
1
2
3

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

# 作用与优缺点

闭包的作用有两个:私有变量和延迟变量的存在时间

作用

  • 1、保护:保护函数的私有变量不受外部的干扰。
  • 2、保存:把一些函数内的值保存下来,闭包可以实现方法和属性的私有化。

优点

  • 1、延长局部变量的生命周期。
  • 2、函数外部能够访问到内部变量。
  • 3、可以避免全局污染。

缺点

  • 1、闭包会携带包含其他的函数作用域,因此会比其他函数占用更多的内存。
  • 2、不正当地使用可能会造成内存泄露(不再用到的内存,没有及时释放,就叫做内存泄露)
// 函数一
function fn2() {
  let test = new Array(1000).fill('Picker')
  return function() {
    console.log(11111)
  }
}
 
// 函数二
function fn2() {
  let test = new Array(1000).fill('Picker')
  return function() {
    console.log(test)
    return test
  }
}
 
// fn2Child是fn2函数返回的匿名函数的引用变量
let fn2Child = fn2()
fn2Child()
 
// 如果使用函数二,则会产生内存泄露,需要及时断开对变量的引用才可以阻止内存泄露(使用函数一不会,因为返回的匿名函数中没有使用外部的函数的变量,但依旧属于闭包)
fn2Child = null  // 解决方法:函数调用后,把外部的引用关系置空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

fn2Child = null;函数调用后,把外部的引用关系置空

TIP

js 引擎为了解决内存泄露问题,才有了 垃圾回收机制,能够让 js 自动的管理内存,将内存中不再使用的变量回收掉,然后释放出内存空间。实现原理就是通过 判断当前的变量是否被引用

以下代码块中函数一与函数二的区别就是返回的匿名函数中是否用到了父函数内部的引用变量 test,如果用到了就存在内存泄漏,如果没有用到则 test 变量完全可以被回收。

闭包产生内存泄漏的根本原因就是 没有及时的断开对变量的引用,而不是闭包产生的内存泄露。如果我们对该引用可以进行控制,就可以解决内存泄露的问题。

# 闭包会导致内存占用过多

一般来讲,当函数执行完毕后,局部活动对象就会销毁,内存仅保存全局作用域。但是,闭包的情况不同,closure 函数执行完毕后,其活动对象不会销毁,因为匿名函数的作用域链仍然引用这个活动对象。直到匿名函数被销毁后,closure 函数的活动对象才会被销毁。

由于闭包会携带包含它的函数的作用域,因此会占用更多的内存,过度的使用闭包会导致内存占用过多,因此,在绝对必要时,再考虑使用闭包。

# 垃圾回收与内存泄露

  • 1、由于栈内存小,由系统自动分配空间并回收,所以我们经常说的垃圾回收一般指堆内存的回收。
  • 2、没有引用关系的对象就是垃圾(以下 {name: 'Picker'} 即为垃圾。
let a = {
  name: 'Picker'
}
// 重新赋值
a = [1]
1
2
3
4
5
  • 3、栈内存与堆内存回收时机:
    • 1)栈内存:基本上用完就回收了。
    • 2)堆内存:不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递),只有当一个对象没有任何引用变量引用它时,系统的回收机制才会在核实的时候回收它。