JavaScript 内存管理
JavaScript 在变量(对象、字符串等)创建时自动分配内存,并在它们不再被使用时“自动”释放内存。这个自动释放内存的过程被称为垃圾回收 (Garbage Collection)。
理解内存管理的基础知识可以帮助你避免一些常见的性能问题,特别是在需要长时间运行且性能敏感的游戏中。
内存生命周期
在 JavaScript 中,内存的生命周期大致分为三个阶段:
分配 (Allocate):当你创建变量、函数或对象时,JavaScript 引擎会为你分配内存。
javascriptlet player = { name: 'Rico' }; // 分配内存给对象 let enemies = []; // 分配内存给数组
1
2使用 (Use):在代码中读取和写入变量。
javascriptplayer.name = 'Rico Jr.'; // 写入(修改) console.log(enemies.length); // 读取
1
2释放 (Release):当分配的内存不再需要时,它将被垃圾回收器自动释放。
垃圾回收 (Garbage Collection)
垃圾回收的核心挑战是判断一块内存是否“不再需要”。JavaScript 引擎主要使用一种名为标记-清除 (Mark-and-Sweep) 的算法。
这个算法的基本思想是:
- 从一组根(Roots)对象(例如全局对象)开始,垃圾回收器会找到所有从根可访问的对象,并为它们做上“标记”。
- 任何无法从根访问到的对象都被认为是“垃圾”。
- 垃圾回收器会遍历堆,并清除所有未被标记的对象,释放它们的内存。
你不需要手动触发垃圾回收,但你需要确保不再使用的对象变得“不可访问”,以便回收器能够处理它们。
常见的内存泄漏
内存泄漏是指程序中已分配的内存由于某种原因未被释放,导致它在程序的生命周期中持续占用内存。长时间运行的应用(如游戏)对内存泄漏尤其敏感。
1. 意外的全局变量
如果在函数中忘记使用 let
、const
或 var
声明变量,该变量会成为全局变量,通常不会被回收。
function createEnemy() {
// `name` 成为了全局变量,可能导致泄漏
name = 'Slime';
// 正确的方式: let name = 'Slime';
}
2
3
4
5
使用 'use strict';
可以帮助防止这种情况。
2. 被遗忘的定时器或回调
如果你设置了一个 setInterval
,但忘记在不再需要时清除它,那么定时器引用的任何对象都无法被回收。
function startCombat() {
let enemy = { id: 1, health: 100 };
let timer = setInterval(() => {
console.log(`敌人 ${enemy.id} 正在攻击...`);
// 假设战斗结束后,我们忘记了清除定时器
// if (combatOver) { clearInterval(timer); }
}, 1000);
// 如果不调用 clearInterval,`timer` 和它引用的 `enemy` 对象将永远不会被回收
}
2
3
4
5
6
7
8
9
10
11
12
13
解决方法:始终确保在组件销毁或不再需要时,使用 clearInterval
或 removeEventListener
来清理定时器和事件监听器。
3. 闭包引起的泄漏
闭包会使其外部函数的变量保持活动状态。如果闭包本身被长时间引用,这些变量也无法被回收。
function createLeakyClosure() {
let largeObject = new Array(1000000).join('*');
// 这个返回的函数维持了对 largeObject 的引用
return function() {
return largeObject.length;
};
}
// 即使我们只需要这个函数,largeObject 也会一直存在于内存中
let leakyFunction = createLeakyClosure();
2
3
4
5
6
7
8
9
10
11
解决方法:确保不再需要时,解除对闭包的引用(例如,leakyFunction = null;
)。
虽然现代 JavaScript 引擎的垃圾回收器非常高效,但养成良好的编码习惯,警惕潜在的内存泄漏,是构建高性能、稳定应用的关键。