面试题答案
一键面试原始类型和引用类型在垃圾回收机制下资源管理的不同
- 存储方式:
- 原始类型:包括
string
、number
、boolean
、null
、undefined
、symbol
(ES6 新增),它们的值是直接存储在栈(stack)中的简单数据段,占据固定大小的空间。例如:
- 原始类型:包括
let num = 10;
这里的 num
直接在栈中存储值 10
。
- 引用类型:如
Object
、Array
、Function
等,它们的值是存储在堆(heap)中的对象。在栈中存储的是一个指向堆中实际对象的引用地址。例如:
let obj = {name: 'John'};
栈中 obj
存储的是指向堆中 {name: 'John'}
这个对象的引用。
2. 垃圾回收机制:
- 原始类型:由于它们在栈中存储,当它们超出作用域时,栈会自动清理相关数据,不需要垃圾回收机制主动干预。例如:
function test() {
let num = 10;
}
test();
// 函数执行完毕,num 超出作用域,栈自动清理 num 的数据
- 引用类型:垃圾回收机制通过标记清除算法或引用计数算法来管理它们的内存。标记清除算法会定期标记所有从根对象(如全局对象)可达的对象,然后清除那些未被标记的对象(即不可达对象)。引用计数算法则是跟踪每个对象被引用的次数,当引用次数为 0 时,回收该对象的内存。但引用计数算法存在循环引用的问题,例如:
function circularReference() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2;
obj2.a = obj1;
}
circularReference();
// 这里 obj1 和 obj2 相互引用,在引用计数算法下,即使函数执行完毕,它们引用次数都不为 0,导致内存泄漏
在现代 JavaScript 引擎中,主要采用标记清除算法来避免循环引用导致的内存泄漏问题。
在复杂应用场景中避免内存泄漏的方法及实际案例
- 避免原始类型导致的内存泄漏:
- 合理使用闭包:闭包可能会导致原始类型数据在不该存在的时候依然存在于内存中。例如:
function outer() {
let num = 10;
return function inner() {
console.log(num);
};
}
let closure = outer();
// 这里闭包 inner 引用了 outer 作用域中的 num,即使 outer 执行完毕,num 依然不能被清理
// 避免方法:如果不再需要闭包对 num 的引用,可以手动将闭包设为 null
closure = null;
- 避免引用类型导致的内存泄漏:
- 事件监听移除:在 DOM 操作中,给元素添加事件监听器后,如果在元素被移除时没有移除事件监听器,会导致内存泄漏。例如:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF - 8">
<title>内存泄漏示例</title>
</head>
<body>
<button id="btn">点击</button>
<script>
let btn = document.getElementById('btn');
let data = {message: '一些数据'};
btn.addEventListener('click', function () {
console.log(data.message);
});
// 假设之后要移除按钮
document.body.removeChild(btn);
// 这里虽然按钮从 DOM 中移除,但事件监听器依然引用着 data,导致 data 不能被回收
// 正确做法是移除事件监听器
btn.removeEventListener('click', function () {
console.log(data.message);
});
document.body.removeChild(btn);
</script>
</body>
</html>
- 解除循环引用:如前面提到的循环引用案例,在不需要对象相互引用时,手动解除引用。例如:
function circularReference() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2;
obj2.a = obj1;
// 解除循环引用
obj1.a = null;
obj2.a = null;
}
circularReference();
// 这样在函数执行完毕后,obj1 和 obj2 可以被垃圾回收机制回收
- 避免不合理的全局变量:全局变量在整个应用生命周期内都存在,如果不合理使用,可能导致内存泄漏。例如:
// 错误做法
function add() {
window.result = 1 + 2;
}
add();
// 这里在全局对象 window 上添加了 result 变量,即使 add 函数执行完毕,result 依然存在于内存中
// 正确做法是使用局部变量
function add() {
let result = 1 + 2;
return result;
}
let res = add();