前言
在JavaScript中,闭包是一种强大而灵活的特性,它不仅允许变量私有化,而且提供了一种在函数执行完毕后仍然保持对外部作用域变量引用的机制。本文将深入讨论JavaScript闭包的概念、优点、缺点以及如何避免潜在的内存泄漏问题。
调用栈与作用域链
在理解闭包之前,首先需要了解调用栈和作用域链的概念。
调用栈
调用栈是用来管理函数调用关系的数据结构。当一个函数执行时,会将其执行上下文推入调用栈,如下图所示:
当函数执行完毕后,它的执行上下文就会从调用栈中弹出,如下图:
作用域链
作用域链是通过词法作用域(静态作用域)来确定某个作用域的外层作用域,在查找变量时,会按照由内而外的链状关系进行查找。这种链状关系叫做作用域链
- 对于使用
var
声明的变量,它们位于变量环境。 - 对于使用
let
和const
声明的变量,它们位于词法环境。 - outer属性指向外层作用域,全局执行上下文的outer指向null
- outer的值取决于函数声明在何处而非在何处调用
如下图:
从bar的执行上下文到全局执行上下文以及foo的执行上下文到全局上下文的这种查找的链状关系,就叫做作用域链。
闭包的概念
闭包是指能够访问其外部函数中声明的变量的函数,即使外部函数执行完毕。在JavaScript中,由于词法作用域的存在,内部函数总是可以访问外部函数中声明的变量。我们来看下一个例子:
function foo() {
var myName ='旭旭'
let test1 = 1
let test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName('浪哥')
console.log(bar.getName());
在上面的例子中,foo
函数在执行完毕后,产生了一个闭包,内容为myName = '旭旭'
和test = 1
,当foo()
执行完成后,垃圾回收机制将foo
的执行上下文清理掉了,但是由于,foo
函数中的innerBar
对象中的,getName
函数以及setName
函数中存在对test1
与myName
的引用,所以在垃圾回收机制执行后,留下了myName = '旭旭'
和test = 1
,他们的集合称作闭包。即,下图黄框部分:
闭包的简单应用
我们先来看这样一段代码:
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
//------
for (var j = 0; j < arr.length; j++) {
arr[j]()
}
代码看上去,像是要完成输出0-9
的功能,但是实际上的输出结果为10
个10
,因为在for循环
声明的i
是由var
声明的,var
存在声明提升,所以相当于在全局声明的i
,而当第一个for循环
结束后,i
达到了10
,并且声明了10
个函数体,存到了数组arr[]
中,在第二次的for循环
中,将arr
的10
个函数体取出并且调用,调用结果为打印i
,而此时的i
为10
,所以会输出10
个10
。
如果我们要在改动最小的情况下,使它的功能变为打印0-9
那么我们可以将第一个for循环
中的i
,改为使用let
声明
因为使用let
声明i
的时候,每次执行for循环
都会形成一个块级作用域
,而在执行输出i
的语句时,我们会首先在这个形成的块级作用域查找,从而完成每个作用域中的i
保留为0-9
的值,所以在输出时能够实现输出0-9
但是如果我们的第一个for循环
仍要使用var
声明i
,那么我们就可以利用闭包来实现输出0-9
的功能,代码如下:
var arr = []
for (var i = 0; i < 10; i++) {
(arr[i] = function (j) {
console.log(j)
})(i)
}
在这个过程中我们直接在创建函数的时候,直接对其调用,从而利用闭包的把i
此时的值留住,形成闭包,闭包中的内容为arr[i]
,arr[i]
中的内容为function(j){console.log(j)}
,j
为i
,所以输出0-9
。
闭包的优点
变量私有化
闭包允许在内部函数中访问外部函数的变量,从而实现变量的私有化。这种机制在框架级别的开发以及一些设计模式中非常有用,可以避免变量被外部随意修改。
function counter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const increment = counter();
increment(); // 输出 1
increment(); // 输出 2
在上面的例子中,count
变量被私有化在 counter
函数内部,外部无法直接访问或修改它。
闭包的缺点
内存泄漏
闭包的一个潜在问题是内存泄漏。由于闭包使得内部函数保持对外部函数作用域中变量的引用,如果这些引用没有被及时释放,可能导致内存占用过高。
function createHeavyObject() {
const heavyObject = /* 创建一个占用大量内存的对象 */;
return function() {
console.log(heavyObject);
};
}
const myClosure = createHeavyObject();
// 此时myClosure包含对createHeavyObject函数作用域中heavyObject的引用
在上述例子中,myClosure
包含对 createHeavyObject
函数作用域中 heavyObject
的引用,即使外部不再需要 heavyObject
,它依然无法被垃圾回收。要避免内存泄漏,可以手动解除对不再需要的引用,或者使用一些优化手段。
如何避免内存泄漏
为了避免闭包导致的内存泄漏,可以采取以下措施:
1. 及时解除引用
当不再需要闭包时,手动将对外部作用域变量的引用解除,让垃圾回收机制能够回收相关资源。
function createHeavyObject() {
const heavyObject = /* 创建一个占用大量内存的对象 */;
return function() {
console.log(heavyObject);
};
}
const myClosure = createHeavyObject();
// 手动解除引用
myClosure = null;
2. 使用垃圾回收优化
一些现代 JavaScript 引擎会对闭包进行优化,自动检测不再需要的引用并进行回收。但这并不是一劳永逸的解决方案,仍然建议在代码中注意及时释放不再需要的引用。
结语
闭包是JavaScript中强大而灵活的特性,能够提供变量私有化的能力。然而,要小心使用闭包,以防止潜在的内存泄漏问题。及时释放不再需要的引用是保持代码健康的重要步骤,合理利用闭包将为你的代码带来便利和安全性。
如果有疑问或者错误,欢迎在评论区指出!
原文链接:https://juejin.cn/post/7300779577059131402