什么是垃圾?
在js中,垃圾通常指的是不再被程序使用的内存或对象。也就是说,垃圾是指程序中分配的内存空间或对象,但不再被程序使用或无法被访问到的内容
function createIncrease() {
const doms = new Array(100000).fill(0).map((item, i) => {
const dom = document.createElement("div");
dom.innerHTML = i;
return dom;
});
function _increase() {
doms.forEach((dom) => {
dom.innerHTML = Number(dom.innerHTML + 1);
});
}
return _increase;
}
const increase = createIncrease();
const btn = document.querySelector("#btn")
btn.addEventListener("click",increase)
上面的代码当执行createIncrease
方法时,会创建一个increase
方法,同时也会创建一个拥有100000个元素的doms
数组,当btn
点击时执行increase
方法。此时的doms
肯定不能称之为垃圾
,因为当btn
点击时需要使用doms
,这种判断相对来说比较简单
const nums = [1, 2, 3, 4, 5];
const sum = nums.reduce((total, num) => total + num, 0);
上面的代码sum
是由nums
的每一项相加得出的结果,至于nums
还会不会再次被使用,根本没法判断
因此,从上面对垃圾
的阐述以及例子可以看出,是否为垃圾
是由开发者自己决定的。通俗点说,当一段代码执行完成后不会再被使用,那么可以称之为垃圾
垃圾回收
垃圾回收是JavaScript中一种自动内存管理机制,用于检测和释放不再使用的对象,以便回收内存。垃圾回收器会定期扫描内存中的对象,并标记那些仍然被引用的对象,然后清除未被引用的对象垃圾回收器。换句话说,垃圾回收只回收
那些开发者自己都访问不了
,无法触达
的内存空间
垃圾回收算法:JavaScript 垃圾回收器使用不同的算法来确定内存中的对象是否可以被回收。常见的算法包括标记-清除
、引用计数
和增量标记
等。标记-清除是最常用的算法,它通过标记不再需要的对象,然后清除它们来回收内存
function createIncrease() {
const doms = new Array(100000).fill(0).map((item, i) => {
const dom = document.createElement("div");
dom.innerHTML = i;
return dom;
});
function _increase() {
doms.forEach((dom) => {
dom.innerHTML = Number(dom.innerHTML + 1);
});
}
return _increase;
}
const increase = createIncrease();
const btn = document.querySelector("#btn");
const flag = true;
function handleClick() {
flag = false;
if (flag) {
increase();
btn.removeEventListener("click", handleClick);
increase = null;
}
}
btn.addEventListener("click", handleClick);
const nums = [1, 2, 3, 4, 5];
const sum = nums.reduce((total, num) => total + num, 0);
nums = null
比如上面的代码:
- 当
btn
被点击时执行increase
方法,当点击完成后,移除btn
点击事件以及将increase
设置为null
,此时就会被垃圾回收器回收 - 将
nums
重新赋值为null
时,那么之前声明的nums
的内容我们就无法再访问,因此就会被回收
内存泄漏
内存泄漏是指当程序中的对象不再被使用,但由于某些原因仍然被保留在内存中,导致内存占用不断增加,最终耗尽可用内存的现象。
const timer = setTimeout(() => {
// todo
}, 100);
const nums = [1, 2, 3, 4, 5];
const sum = nums.reduce((total, num) => total + num, 0);
上面的代码我们声明了一个timer
的定时器和一个nums
的数组,timer
没有清理,nums
我们明确以后不会再使用,但是timer
、nums
会一直存在内存中,这样就容易引起内存泄漏
常见的内存泄漏情况包括:
未及时清除的计时器
:如果你创建了一个计时器,但忘记在不再需要时清除它,它将一直存在并持有对相关对象的引用,导致内存泄漏未解绑的事件监听器
:如果你绑定了一个事件监听器,但在元素被销毁之前忘记解绑它,这个监听器将一直存在,并可能持有对相关对象的引用,导致内存泄漏循环引用
:如果对象之间存在循环引用(例如,对象 A 持有对象 B 的引用,同时对象 B 也持有对象 A 的引用),即使它们不再被使用,垃圾回收器也无法释放它们的内存,导致内存泄漏
当内存泄漏过大时会严重影响我们的代码,因为在开发时为避免内存泄漏,可以采取以下措施:
- 尽量避免创建不必要的闭包,当不再需要时,显式地解除对闭包的引用
- 将不再使用的变量设置为 null,以协助垃圾回收
- 尽量避免循环引用,特别是在闭包中
- 仔细管理计时器、事件监听器和其他可能持有对对象的引用的机制
- 使用优化的数据结构和算法,避免不必要的内存占用
闭包
闭包是指一个函数能够访问和操作在其词法作用域以外定义的变量。闭包在JavaScript 中被广泛应用,它可以用于创建私有变量、实现模块化等。由于闭包会使函数引用外部变量,当闭包存在时,垃圾回收器可能无法释放被闭包引用的对象,导致
内存泄漏
function createIncrease() {
const doms = new Array(100000).fill(0).map((item, i) => {
const dom = document.createElement("div");
dom.innerHTML = i;
return dom;
});
function _temp() {
return doms;
}
function _increase() {}
return _increase;
}
let increase;
btn.addEventListener("click", () => {
increase = createIncrease();
});
上面的代码当点击事件被执行时对increase
重新赋值,而createIncrease
方法返回_increase
方法,因此,此时doms
是一块无法触达
的内存空间,那么此时的这块无法触达的内存空间会被回收吗?
未点击之前
点击后
从上面的图片可以看出,doms
并不会被回收,这是因为_temp
和_increase
方法的闭包环境是一样的,它们共用一个词法环境,因此doms
不会被回收
从上面的例子可以看出,闭包导致内存泄漏的原因有:
- 持有了不再需要的函数引用,会导致函数关联的词法环境无法销毁,导致内存泄漏
- 当多个函数共享词法环境时,会导致词法膨胀,从而使一些无法触达的内存空间无法回收,导致内存泄漏