1、哪些情况会导致内存泄漏?如何排查和解决?
答:内存泄漏:本该被垃圾回收(GC) 释放的内存,因为仍然被引用着而无法释放。常见场景:
- 全局变量:不小心在函数里漏写
var/let/const,变量变成全局的;会持久存在,GC无法回收。
function foo() {
leak = 'I am a global var'; // 忘记声明
}
- 定时器/回调未清理:
setInterval没有clearInterval;绑定的事件监听器addEventListener没卸载。
const timer = setInterval(() => console.log('leak'), 1000);
- 闭包使用不当:函数引用了外部变量,但函数一直存在,导致外部变量无法回收。
function outer() {
let bigData = new Array(1000000).fill('xxx');
return () => console.log(bigData[0]); // 持有引用
}
const leakFn = outer();
DOM引用未释放:移除了DOM元素,但JS中还保留了变量引用。
let btn = document.getElementById('button');
document.body.removeChild(btn);
// btn = null 清除,Vue和React框架内一般或自动清理掉,但定时器要手动清理
- 缓存/数据结构未清理:使用
Map/Set保存了大量数据,但从不清理。单页应用(SPA)里路由切换,组件没卸载干净。
const cache = new Map();
cache.set('key', new Array(1000000));
// cache.clear(); 清理
// cache = null;清理
- 事件总线/发布订阅未解绑:比如
Vue/React中的EventEmitter/PubSub没有off。
注:
- 基本类型:在
Map/Set里存的是值,不存在GC回收的问题。- 引用类型:在
Map/Set里存的是强引用,不手动清理就不会被回收。- 解决办法:用
WeakMap/WeakSet,它们存的是弱引用,对象没引用后会被GC回收。
如何排查内存泄漏
Chrome DevTools → Performance/Memory- 打开
Performance,录制运行 → 查看内存曲线是否持续上升。 - 使用
Memory → Heap snapshot,对比前后两次快照,看哪些对象一直存在。
- 打开
Timeline → Memory Profiler- 长时间运行时观察堆内存变化,是否出现 “锯齿状”(正常 GC)还是只涨不降(泄漏)。
Performance API- 使用
performance.memory.usedJSHeapSize监控内存占用(仅Chrome提供)。
- 使用
- 断点排查
- 如果知道大概泄漏位置,可以在相关对象销毁时打断点,检查引用链。
解决思路
- 全局变量 → 避免直接挂到
window,使用局部作用域或模块化。 - 闭包 → 谨慎使用,确保不必要的引用及时解除。
- DOM/事件 → 在组件销毁时
removeEventListener、置null,避免引用残留。 - 定时器 → 使用
clearTimeout/clearInterval,或组件卸载时清理。 - 缓存 → 使用
WeakMap/WeakSet存储对象,避免强引用。 - 框架组件 → 确保在生命周期里清理资源(
React/Vue等都有对应钩子)。 - 工具 → 定期使用
Chrome DevTools快照检查。
2、Promise 的基本原理是什么?
答:Promise是异步编程的一种解决方案,它的核心思想是维护一个状态机:初始状态(pending)、成功状态(fulfilled)、失败状态(rejected)
特点:
- 状态不可逆:从
pending → fulfilled或pending → rejected,一旦确定就不能再改变。 - 回调收集:通过
.then()注册回调,在状态改变后依次执行。 - 链式调用:
.then()会返回一个新的Promise,从而支持链式操作。 - 异步执行:
Promise的回调会被放到微任务队列,不会阻塞主线程。
3、Promise.all / Promise.race / Promise.any / Promise.allSettled 区别?
答:
Promise.all:所有Promise成功才成功;有一个失败立即失败;成功:按顺序返回数组;任何一个失败 → 整个失败。应用场景:并发请求依赖所有数据(比如首页多个接口数据)。Promise.race:谁先结束(无论成功/失败),返回第一个结果(值或错误),第一个失败就失败;典型场景:请求超时控制(竞速)Promise.any:任意一个成功就成功,返回第一个成功的结果,如果全部失败 → 返回AggregateError;典型场景:多 CDN / 多资源兜底,只要一个成功即可Promise.allSettled:等待全部结束(不论成功/失败),每个结果对象 {status, value/reason},不会中断;典型场景:批量请求结果统计或展示
Promise.any和Promise.race的区别:race只看谁先结束,可能是 reject;any只关心谁先成功,所有失败才报错。all和allSettled的区别:all遇到错误会短路,剩下的不会继续等;allSettled会等到所有任务完成,不管成败。
4、async / await 的底层原理?相当于什么语法糖?
答:async函数始终返回一个Promise。await表达式会 暂停async函数的执行,等待Promise resolve,然后继续执行。底层实现依赖Promise + 微任务队列。换句话说:async/await 是基于Promise的语法糖,让异步写法看起来像同步。
底层执行原理
- 调用
async函数:返回一个Promise,内部代码立即执行到第一个await表达式。 - 遇到
await:- 暂停当前
async函数的执行(不阻塞主线程) await后面的表达式(后面所有的代码,包含同步代码)会被Promise.resolve()包装Promise完成后,把后续代码放入微任务队列执行
- 暂停当前
- 继续执行
- 微任务队列调度时,
async函数继续从暂停点执行 - 遇到下一个
await再重复这个过程
- 微任务队列调度时,
可以理解为:
JS引擎把async函数拆成若干个Promise 链 + 微任务调度。