小蜗熊的蜂蜜罐
JavaScript闭包原理与垃圾回收机制
发布于: 2020-06-16 更新于: 2020-06-29 分类于: 技术 > Web 阅读次数: 

JavaScript中的闭包是指能够访问其他函数作用域内变量的函数,即一个函数内部的函数。闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰,即形成一个不销毁的栈环境。但由于闭包会使得函数中的变量都被保存在内存中,所以滥用闭包会造成性能问题或是导致内存泄漏。

JavaScript闭包简介

首先来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = 1;
function fn1() {
a += 1;
return a;
}
//函数中的a为全局变量
function fn2() {
var a = 1;
a += 1;
return a;
}
//函数中的a为局部变量
fn1();
console.log(fn1());
//控制台输出3
fn2();
console.log(fn2());
//控制台输出2

在这个例子中,fn1使用的是全局变量a,fn2使用的是局部变量a。多次执行函数后,f1返回的是多次累加的结果,而f2由于使用的是局部变量执行结果始终与单次执行相同。如果希望多次对一个变量进行操作,同时又不希望将其暴露在全局之中,此时上面的两个函数均无法满足需求,可使用嵌套函数的方法。在JavaScript中,所有函数都能访问它们上一层的作用域。嵌套函数可以访问上一层的函数变量。例如:

1
2
3
4
5
6
7
8
9
10
11
var add = (function f1() {
var counter = 0;
return function f2() {
return counter += 1;
}
})();

add();
add();
add();
//此时counter为3

上面的这个函数就是一个JavaScript闭包,它使得函数拥有私有变量变成可能。

作用域与作用域链

JavaScript中存在如下三种作用域:

1
2
3
4
5
6
7
// 全局作用域
function func() {
// 函数作用域
{
// 块级作用域
}
}

当同时存在多个作用域对象时,所有函数的作用域对象会被环境栈所管理。环境栈中的作用域对象是按顺序访问的,最先能够访问的是当前函数的作用域,如果访问的变量在当前作用域没有,会访问上一层作用域,直到找到全局作用域。如果访问到全局作用域也没有这个对象,会抛出ReferenceError的异常。

简单来说就是前面提到过的所有函数都能访问它们上一层的作用域,子对象会一级级向上寻找所有父对象的变量,函数内部可以读取全局变量,这就是所谓的作用域链。

JavaScript闭包原理

还是以上面提到的闭包为例,上面的例子中多次调用add函数后变量counter的值能够持续增加,原因就在于每次执行完add之后counter并没有被回收。
简单来说,其过程就是:

  • 全局变量add引用了函数f1自我调用后的返回值,即函数f2。
  • f1在自我调用时只执行一次,并将counter初始化为0。
  • add变量可以作为一个函数使用,它可以访问函数上一层作用域的变量,即f1中的counter。
  • 在执行完一次add()之后,由于f2被全局变量add引用了,所以不会被回收销毁。
  • 而f1又引用了f2中的变量,所以f2作用域也不会被销毁,下一次执行时counter值不会重置。

这样一来,就可以对变量counter进行多次操作,同时counter受函数f1的作用域保护,只能通过add方法修改,不会暴露在全局中,上一节中提到的需求通过闭包实现了。
闭包产生的三个必要条件为:

  • 在函数A内部直接或者间接返回一个函数B
  • B函数内部使用着A函数的私有变量(私有数据)
  • A函数外部有一个变量接受着函数B

JavaScript垃圾回收机制

JavaScript具有自动垃圾回收机制。后台的垃圾回收器会监视所有对象,按照固定的时间间隔周期性地执行垃圾回收,删除那些不可达的对象。
对象的可达性遵循下面的原则:

  • 本地函数的局部变量和参数、当前嵌套调用链上的其他函数的变量和参数、全局变量等是可达的。
  • 如果一个对象被引用,则它是可达的
  • 如果若干个对象互相引用成环,且没被其他对象引用,则这些对象都是不可达的
--- 本文结束 The End ---