JS闭包与垃圾回收机制详解:原理、应用及内存管理
本文深入解析JavaScript中的闭包概念及其在内存管理中的作用,详细阐述标记清除与引用计数两种垃圾回收算法。通过计数器、定时器顺序输出等经典案例,展示闭包的创建方式、优缺点及常见应用场景,帮助开发者理解变量生命周期与内存优化策略。
前言
闭包与垃圾回收机制是前端开发中的核心难点,也是技术面试的高频考点。本文旨在梳理学习与实践过程中的相关笔记,帮助读者系统掌握这些概念。
正文
1. 闭包
闭包(closure)既是JavaScript语言的难点,也是其重要特性。许多高级编程技巧都依赖于闭包,因此理解闭包对于JavaScript开发者至关重要。
1.1 闭包是什么?
闭包是指内部函数引用了外部函数的变量,当内部函数被返回到外部作用域并保存时,便形成了闭包。此时,内部函数的作用域链(AO)包含了外层函数的活动对象(AO)。
由于变量被引用,它们不会被垃圾回收机制销毁,这使得闭包能够封装私有变量。然而,不必要的闭包会增加内存消耗。闭包本质上是一种保护私有变量的机制,在函数执行时形成私有作用域,防止外部干扰。简而言之,闭包允许子函数访问父函数的局部变量和参数。
1.2 闭包的特性
① 函数嵌套函数
② 函数内部可以访问外部函数的参数和变量
③ 这些被引用的参数和变量不会被垃圾回收机制回收
1.3 理解闭包
基于作用域链的知识,我们来看一个计数器问题:如何实现一个函数,每次调用时计数器加一?
var counter=0;
function demo3(){
console.log(counter+=1);
}
demo3();//1
demo3();//2
var counter=5;
demo3(); //6
如果在全局作用域中直接操作counter变量,任何地方修改counter值都会导致计数器失效。JavaScript通过闭包解决此问题,即在函数内部嵌套函数。
function add() {
var counter = 0;
return function plus() {
counter += 1;
return counter
}
}
var count=add()
console.log(count())//1
var counter=100
console.log(count())//2
上述示例展示了闭包的一个实例:函数add内部嵌套了plus函数,count变量引用了返回的plus函数。每次执行外部函数add时,都会开辟新的内存空间,生成不同的地址。将plus函数嵌套在add内部,创建了counter局部变量。每次调用count函数,该局部变量值加一,从而实现了真正的计数器功能。
1.4 闭包的主要实现形式
主要通过以下两种形式来理解闭包:
① 函数作为返回值,即上述示例中使用的形式。
function showName(){
var name="xiaoming"
return function(){
return name
}
}
var name1=showName()
console.log(name1())
闭包是能够读取其他函数内部变量的函数,它是连接函数内部与外部的桥梁。
② 闭包作为参数传递
var num = 15
var foo = function(x){
if(x>num){
console.log(x)
}
}
function foo2(fnc){
var num=30
fnc(25)
}
foo2(foo)//25
在上述代码中,函数foo作为参数传入foo2。执行foo2时,25作为参数传入foo。此时判断x>num中的num取值来自创建函数时的作用域,即全局的num(值为15),而非foo2内部的num(值为30),因此打印出25。
1.5 闭包的优缺点
优点:
① 保护函数内的变量安全,实现封装,防止变量污染其他环境或发生命名冲突。
② 在内存中维持一个变量,可用于缓存(但使用过多会增加内存消耗,这也是缺点)。
③ 匿名自执行函数可以减少内存消耗。
缺点:
① 被引用的私有变量无法被销毁,导致内存消耗增加,可能引发内存泄漏。解决方法是在使用完变量后手动将其赋值为null。
② 由于闭包涉及跨作用域访问,可能导致性能损失。可以通过将跨作用域变量存储在局部变量中,然后直接访问局部变量来减轻对执行速度的影响。
1.6 闭包的使用
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log( i);
}, 1000);
}
console.log(i);
看一个常见面试题:以下代码输出什么?大多数人知道结果是5个5。但仔细观察,这6个5的输出顺序是怎样的?即5 -> 5,5,5,5,5。理解同步与异步的人不难理解这种情况。接下来思考如何实现0 -> 1,2,3,4的顺序输出?
for (var i = 0; i < 5; i++) {
(function(j) { // j = i
setTimeout(function() {
console.log( j);
}, 1000);
})(i);
}
console.log( i);
//5 -> 0,1,2,3,4
这样在for循环中加入匿名函数,匿名函数入参是每次的i的值,在同步函数输出5的一秒之后,继续输出01234。
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
console.log( i);
//5 -> 0,1,2,3,4
仔细查看setTimeout的API,它还有第三个参数,这样可以省去通过匿名函数传入i的步骤。
var output = function (i) {
setTimeout(function() {
console.log(i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i); // 这里传过去的 i 值被复制了
}
console.log(i);
//5 -> 0,1,2,3,4
这里利用闭包将函数表达式作为参数传递到for循环中,同样实现了上述效果。
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
//5 -> 0,1,2,3,4
了解let块级作用域的人可能会想到上述方法。但如果要实现0 -> 1 -> 2 -> 3 -> 4 -> 5的顺序输出呢?
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(new Date, j);
}, 1000 * j); // 这里修改 0~4 的定时器时间
})(i);
}
setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
console.log(new Date, i);
}, 1000 * i);
//0 -> 1 -> 2 -> 3 -> 4 -> 5
还可以使用Promise来实现。
const tasks = [];
for (var i = 0; i < 5; i++) { // 这里 i 的声明不能改成 let,如果要改该怎么做?
((j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, j);
resolve(); // 这里一定要 resolve,否则代码不会按预期 work
}, 1000 * j); // 定时器的超时时间逐步增加
}));
})(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000); // 注意这里只需要把超时设置为 1 秒
});
//0 -> 1 -> 2 -> 3 -> 4 -> 5
const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, i);
resolve();
}, 1000 * i);
});
// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i);
}, 1000);
});
//0 -> 1 -> 2 -> 3 -> 4 -> 5
// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { // 声明即执行的 async 函数表达式
for (var i = 0; i < 5; i++) {
if (i > 0) {
await sleep(1000);
}
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();
//0 -> 1 -> 2 -> 3 -> 4 -> 5
上述代码中都用到了闭包。总之,闭包找到的是同一地址中父级函数里对应变量的最终值。
2. 垃圾回收机制
JavaScript中的内存管理是自动执行且不可见的。创建基本类型、对象、函数等都需要占用内存。
通常采用的垃圾回收方法有两种:标记清除法和引用计数法。
1. 标记清除
垃圾收集器运行时,会给内存中的所有变量加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。
在此之后,仍然带有标记的变量被视为准备删除的变量,因为环境中的变量已经无法访问到它们。
最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收它们所占用的内存空间。
2. 引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型赋值给该变量时,这个值的引用次数就是1。
相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,
说明没有办法再访问这个值了,因而就可以将其所占的内存空间收回来。这样,垃圾收集器下次再运行时,
它就会释放那些引用次数为0的值所占的内存。
总结
到此这篇关于JS闭包和垃圾回收机制的文章就介绍到这了,更多相关JS闭包和垃圾回收内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
