Js内存回收机制

什么是内存回收?

垃圾回收是指自动释放不再使用的内存。程序在运行时会动态分配内存,但并不是所有被分配的内存都会一直被使用。垃圾回收器的任务就是识别那些不再被使用的内存,并将其释放回系统,以便其他进程可以使用。

标记清除(Mark-Sweep)

  1. 当函数被调用时,一个新的执行上下文(Execution Context)被创建,变量和参数被创建并加入到这个环境中。这个过程称为变量进入环境。

  2. 每个执行上下文都有自己的作用域链,它决定了变量和函数的可见性。作用域链从当前上下文开始,向上遍历至全局上下文。

  3. 变量的生命周期从它被创建开始,直到它离开作用域。在变量的生命周期内,它可以被代码访问和使用。

  4. 当一个执行上下文被销毁,比如函数执行完毕,这个上下文中的变量就被认为是“离开环境”。此时,这些变量不再被代码所引用。

  5. 一旦变量离开环境并且没有任何外部引用指向它们,它们就成为垃圾回收器的候选对象。垃圾回收器会定期检查这些无用的对象,并释放它们占用的内存。

  6. 如果一个变量在离开环境后仍然被某些引用所持有,它就不会被回收。例如,如果一个变量被赋予给一个全局变量或者被一个闭包捕获,那么即使它已经离开了原始作用域,它也不会被回收。

  7. 如果变量在离开环境后仍然被引用,但这些引用不再被需要,就会发生内存泄漏。这种情况下,即使变量不再使用,它们占用的内存也不会被释放。

  8. 垃圾回收不是在变量离开环境的瞬间立即发生的。垃圾回收器会根据一定的算法和策略来决定何时进行回收,以减少对程序性能的影响。

引用计数 (Reference Counting)

  1. 引用计数是一种自动内存管理技术,它为每个值(对象)维护一个计数器,记录该值被引用的次数。

  2. 当一个新变量被声明并赋值为一个引用类型时,该引用类型的引用计数加1。例如,var obj = new Object();此时,obj指向的对象的引用计数为1。

  3. 当一个变量被赋予新值或被删除时,它原来引用的对象的引用计数减1。例如,var obj2 = new Object(); obj = obj2;此时,原始对象的引用计数减1(如果之前没有其他引用指向它),而obj2指向的对象的引用计数加1。

  4. 当一个对象的引用计数降到0时,意味着没有任何变量引用这个对象,它所占用的内存可以被垃圾收集器回收。

内存溢出

内存溢出一般是指执行程序时,程序会向系统申请一定大小的内存,当系统现在的实际内存少于需要的内存时,就会造成内存溢出,内存溢出造成的结果是先前保存的数据会被覆盖或者后来的数据会没地方存。

内存泄露

内存泄漏是指程序执行时,一些变量没有及时释放,一直占用着内存而这种占用内存的行为就叫做内存泄漏。作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积。

闭包

闭包在编程中解决了许多问题,主要包括:

数据封装: 闭包允许将数据和操作这些数据的函数封装在一起,形成一个独立的模块,避免了全局变量的滥用。

持久化状态: 闭包可以保持函数执行后的状态,使得函数可以记住并访问其创建时的环境状态,即使在原始上下文之外被调用。

私有变量: 由于JavaScript不支持传统的私有变量概念,闭包可以用来创建私有变量,保护数据不被外部直接访问。

函数记忆: 闭包可以用于记忆化技术,即缓存函数的计算结果,避免重复计算,提高性能。

延迟计算和资源管理: 闭包可以在需要时才执行计算,实现延迟执行的效果,同时在异步操作完成后进行资源管理。

柯里化: 闭包可以用于实现柯里化,即将一个多参数的函数转换成多个单参数的函数,每个单参数函数返回下一个参数的函数。

高阶函数: 闭包使得JavaScript可以轻松实现高阶函数,即函数可以接受其他函数作为参数或返回函数。

事件处理: 在JavaScript中,闭包常用于事件处理,确保事件处理器可以访问正确的上下文和状态。

异步编程: 在JavaScript的异步编程模式中,闭包可以保持回调函数的上下文,确保在异步操作完成后能够访问到正确的变量和状态。

模块化: 闭包有助于实现模块化编程,通过将函数和相关状态封装在一起,形成独立的模块,提高代码的可维护性和可重用性。

闭包的概念

闭包(Closure)是计算机编程中的一个概念,它指的是一个函数能够记住并访问其创建时的环境的状态,即使这个函数在其原始上下文之外被执行。简单来说,闭包允许一个函数访问创建时的作用域中的变量,即使该函数在其原始作用域之外被调用。

闭包的使用

1
2
3
4
5
6
7
8
9
let a = 10

function test(args) {
args++
return args
}
test(a) //11
test(a) //11
test(a) //11

上述代码中,每次调用 test 函数时, args 都是 a 的一个新副本。当 args 在函数内部被修改时,它并不会影响外部的变量 a 。这就是为什么每次调用 test 函数,传入的 a 值都是原始的 10 ,函数内部将其增加 1 ,然后返回 11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = 10;

function test() {
let args = a;

function inner() {
args++;
console.log(args);
}
return inner;
}

let closure = test(); // 这里创建了一个闭包
closure(); // 11
closure(); // 12
closure(); // 13
closure = null; // 释放闭包

上述代码中, test 函数内部定义了一个名为 inner 的函数,这个 inner 函数形成了一个闭包。闭包允许 inner 函数访问并操作 test 函数的局部变量 args 。即使 test 函数执行完毕后, inner 函数仍然可以访问 args 变量,因为它被闭包捕获了。最后,我们通过 closure = null 释放了闭包。

防抖

防抖的概念

防抖(Debouncing)是一种编程技巧,用于控制函数的执行频率。在JavaScript中,防抖机制特别适用于处理诸如窗口调整大小、滚动、连续的按键输入等频繁触发的事件。它是一种限制函数执行频率的技术,确保函数在指定的时间间隔后执行一次,即使在这个间隔内有多次触发。

防抖的使用

1
2
3
4
5
6
7
8
9
<body>
<input type='text'>
</body>
<script>
let input = document.querySelector('input')
input.oninput = function() {
console.log(this.value)
}
</script>

上述代码中,我们通过 oninput 事件监听输入框的输入事件。每当用户在输入框中输入内容时,就会在控制台打印输入的内容,这会导致输入框的输入事件被频繁触发。但是我们需要的内容只是用户输入的最终结果,而不是中间过程。所以需要对这段代码进行优化,也就是做防抖处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<input type='text'>
</body>
<script>
let input = document.querySelector('input')
let timer = null;
input.oninput = function() {
if(timer!==null) {
cleaTimeout(timer)
}
timer = setTimeout(() => {
console.log(this.value);
},1000);
}
</script>

上述代码中,我们通过 setTimeout 函数来实现防抖。在每次触发输入事件时,都会判断当前是否已经有一个计时器,如果存在,则清除计时器。然后通过 setTimeout 创建一个新的计时器,在指定的时间间隔内执行一次函数。但是这个代码并不优美,不具备可维护性,所以我们可以使用闭包的手段,让这段代码更健壮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<input type='text'>
</body>
<script>
let input = document.querySelector('input');

input.oninput = debounce(() => {
console.log(this.value);
}, 1000);

function debounce(fn, delay) {
let timer = null;
return function() {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn();
}, delay);
};
}
</script>

这样,我们就可以将防抖函数封装成一个通用函数,在需要的时候直接调用即可。