深入JavaScript教你内存泄漏如何防范
myzbx 2025-05-09 20:33 41 浏览
作者:大道至简
转发链接:
https://mp.weixin.qq.com/s/0w6aWwpR3MAJnmyLwDnAzA
前言
一般情况下,忽视内存管理不会对传统的网页产生显著的后果。这是因为,用户刷新页面后,内存数据都被清理了。
但是随着SPA(单页应用)的普及,我们不得不更加关注页面的内存管理。用户在 SPA 上往往很少刷新页面,随着页面停留时间的增长,内存可能越占越多,轻则影响页面性能,严重的可能导致标签页崩溃。
在这篇文章中,我们将探讨导致 JavaScript 中内存泄露的常见原因,以及如何改善内存管理。
具体如何内存管理,请见这篇:「前端进阶」JS中的内存管理
什么是内存泄漏以及如何发现
浏览器将对象保留在堆内存中,通过引用链可从根对象到达这些对象。垃圾回收器(GC)是 JavaScript 引擎中的一个后台进程,它可以识别无法到达的对象,将其删除,并回收相应的内存。
引用链 - GC - 对象关系图
当内存中本应在垃圾回收循环中被清理的对象,通过另一个对象意外的引用从而维持可访问状态,就会发生内存泄漏。将多余的对象保持在内存中,会导致应用程序内部的内存使用量过大,进而影响性能。
内存泄露
如何判断代码是否存在内存泄漏呢?内存泄漏通常比较隐蔽,难以发现和定位。造成内存泄漏的 JavaScript 代码看上去挺正常,浏览器在运行的时候也不会抛出错误。如果发现页面性能越来越差,通常是内存泄漏的征兆,可以通过浏览器内置的工具判断是否存在内存泄漏,并分析出原因。
最快的方法是查看浏览器的任务管理器(注意,不是操作系统的任务管理器)。它提供了浏览器运行中的所有 tab 页和进程的资源使用情况,比如内存占用、CPU 占用和进程 ID 等。Chrome 的任务管理器可通过 Shift+Esc 快捷键打开,Firefox 可在地址栏输入about:performance打开。
如果页面都没有任何交互,内存占用却越来越多,很可能存在泄漏。
Chrome 任务管理器
浏览器 DevTools 则提供了更丰富的内存管理功能。可以在 Chrome 的性能面板录制页面运行情况,查看可视化的性能分析数据。
Chrome 性能面板
除此之外,Chrome 和 Firefox 的 DevTools 还有专门的内存工具用于分析内存使用情况。通过比较连续的内存快照,可以看出内存分配情况。
通过前面的分析,内存泄露的根本原因就是代码在无意之中引用了本该被 GC 回收的对象。那么,哪些情况容易造成内存泄露呢?
1意外的全局变量
全局变量一直处于可访问状态,不会被 GC 回收。在非严格模式下,有时会不小心让局部变量变成全局变量。
- 给未声明的变量赋值
- 使用指向全局对象的 this
function createGlobalVariables() {
leaking1 = '变成全局变量了'; // 给未声明的变量赋值
this.leaking2 = '这也是全局变量'; // 'this' 指向全局对象
};
createGlobalVariables();
window.leaking1; // '变成全局变量了'
window.leaking2; // '这也是全局变量'如何避免: 严格模式 ("use strict") 会避免意外的全局变量,以上代码在严格模式下会报错。
2 闭包
函数作用域变量在函数执行完后会被清理,前提是在函数外部没有引用它。闭包会让变量一直处于被引用状态,即使它的执行上下文和作用域已经不存在了。
function outer() {
const Array = [];
return function inner() {
bigArray.push('Hello');
console.log('Hello');
};
};
const sayHello = outer(); // 包含了对 inner 的引用
function repeat(fn, num) {
for (let i = 0; i < num; i++){
fn();
}
}
repeat(sayHello, 10); // 每次调用 sayHello 都会添加 'Hello' 到potentiallyHugeArray
// 如果是10万次呢?阔怕:repeat(sayHello, 100000)上面例子中的数组 bigArray 没有从任何函数中直接返回,因此无法直接访问,但是它却不停地膨胀,取决于我们调用了多少次 function inner()。
如何避免: 闭包是 JavaScript 语言的特性之一,如果无法避开,那就请注意两点:
- 清楚闭包是何时创建的,以及哪些对象会被保留在内存中;
- 清楚闭包的生命周期和用途(尤其是当做回调函数的时候)
3 定时器
在 setTimeout 或 setInterval 的回调函数中引用某些对象,是防止被 GC 回收的常见做法。如果在代码里设置循环定时器(setTimeout也能像setInterval一样定时重复执行,只要设置成递归调用),只要定时器还在运行,回调函数中的对象就会一直保持在内存中。
下面的例子中,data 对象会在清除定时器后被 GC 回收。但我们没有获取 setInterval的返回值,也就没办法用代码清除这个定时器,因此尽管完全没有用到,data.hugeString 也会一直保留在内存中,直到进程结束。
function setCallback() {
const data = {
counter: 0,
hugeString: new Array(100000).join('x')
};
return function cb() {
data.counter++; // data 对象现在已经属于回调函数的作用域了
console.log(data.counter);
}
}
setInterval(setCallback(), 1000); // 没法停止定时器了如何避免: 对于生命周期不确定的回调函数,我们应该:
- 注意被定时器回调函数引用的对象
- 使用定时器返回的句柄,在必要时清除它
也可以通过分离变量的方式,避免对大对象的引用:
function setCallback() {
// 分开定义变量
let counter = 0;
const hugeString = new Array(100000).join('x'); // setCallback执行完即可被回收
return function cb() {
counter++; // 只剩 counter 位于回调函数作用域
console.log(counter);
}
}
const timerId = setInterval(setCallback(), 1000); // 保存定时器 ID
// 执行某些操作 ...
clearInterval(timerId); // 停止定时器4 事件监听器
活动的事件监听器会阻止作用域内的变量被 GC 回收。事件监听器一直处于活动状态,直到用 removeEventListener() 显式移除,或者关联的 DOM 元素被移除。
对于有些事件来说,监听器需要一直保留,直到页面被销毁。比如按钮点击事件,我们可能需要重复使用。但是,有时候我们希望某个事件只执行特定次数。
const hugeString = new Array(100000).join('x');
document.addEventListener('keyup', function() { // 匿名监听器无法移除
doSomething(hugeString); // hugeString 会一直处于回调函数的作用域内
});上面例子中的事件监听器用了匿名函数,这样就没法用removeEventListener()移除了。同时,document元素也无法删除,因此事件回调函数内的变量会一直保留,哪怕我们只想触发一次事件。
如何避免: 事件监听器不再需要时,要记得解除绑定。使用具名函数方式获取引用,通过removeEventListener()解除绑定。
function listener() {
doSomething(hugeString);
}
document.addEventListener('keyup', listener);
document.removeEventListener('keyup', listener); 如果事件监听器只需要执行一次, addEventListener()可以接受第三个参数,是一个配置对象。指定{once: true},监听器函数会在事件触发一次执行后自动移除(匿名函数也可以)。
document.addEventListener('keyup', function listener(){
doSomething(hugeString);
}, {once: true}); // 执行一次后自动移除事件监听器5 缓存
如果持续不断地往缓存里增加数据,没有定时清除无用的对象,也没有限制缓存大小,那么缓存就会像滚雪球一样越来越大。
let user_1 = { name: "Kayson", id: 12345 };
let user_2 = { name: "Jerry", id: 54321 };
const mapCache = new Map();
function cache(obj){
if (!mapCache.has(obj)){
const value = `${obj.name} has an id of ${obj.id}`;
mapCache.set(obj, value);
return [value, 'computed'];
}
return [mapCache.get(obj), 'cached'];
}
cache(user_1); // ['Kayson has an id of 12345', 'computed']
cache(user_1); // ['Kayson has an id of 12345', 'cached']
cache(user_2); // ['Jerry has an id of 54321', 'computed']
console.log(mapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321")
user_1 = null;
//Garbage Collector
console.log(mapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321") // 依然在缓存里上面的例子中,缓存依然保留了user_1 的数据。因此我们需要把不再使用的数据从缓存中删除。可能的解决方案: 为了解决这个问题,可以使用 WeakMap。 WeakMap 是一种数据结构,它只用对象作为键,并保持对象键的弱引用,如果这个对象被置空了,相关的键值对会被 GC 自动回收。
let user_1 = { name: "Kayson", id: 12345 };
let user_2 = { name: "Jerry", id: 54321 };
const weakMapCache = new WeakMap();
function cache(obj){
// 代码跟前一个例子相同,只不过用的是 weakMapCache
return [weakMapCache.get(obj), 'cached'];
}
cache(user_1); // ['Kayson has an id of 12345', 'computed']
cache(user_2); // ['Jerry has an id of 54321', 'computed']
console.log(weakMapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321"}
user_1 = null;
// Garbage Collector
console.log(weakMapCache); // ((…) => "Jerry has an id of 54321") - 第一条记录已被 GC 删除6 分离的 DOM 元素
如果 DOM 节点被 JavaScript 代码直接引用,即使从 DOM 树分离,也不会被 GC 回收。
下面的例子中,removeChild() 达不到预期效果,堆快照会显示HTMLDivElement处于分离状态,因为有个变量指向了这个div。
function createElement() {
const div = document.createElement('div');
div.id = 'detached';
return div;
}
// 即使调用了deleteElement() ,依然保存着 DOM 元素的引用
const detachedDiv = createElement();
document.body.appendChild(detachedDiv);
function deleteElement() {
document.body.removeChild(document.getElementById('detached'));
}
deleteElement(); // 堆快照显示: detached div#detached如何避免: 一种方法是把DOM 引用限制为局部作用域。
function createElement() {...} //
// DOM 引用位于函数作用域内
function appendElement() {
const detachedDiv = createElement();
document.body.appendChild(detachedDiv);
}
appendElement();
function deleteElement() {
document.body.removeChild(document.getElementById('detached'));
}
deleteElement();总结
对于重要的前端应用,定位和解决 JavaScript 内存问题是一项颇具挑战性的任务。因此,理解典型的内存泄露原因,从而在源头上避免,是做好内存管理的必要工作。希望本文总结的造成内存泄漏的六大来源对你有所启发,在写代码的时候有所防范。
推荐JavaScript经典实例学习资料文章
《手把手教你7个有趣的JavaScript 项目-上「附源码」》
《手把手教你7个有趣的JavaScript 项目-下「附源码」》
《JavaScript 使用 mediaDevices API 访问摄像头自拍》
《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》
《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》
《可视化的 js:动态图演示 Promises & Async/Await 的过程》
《Pug 3.0.0正式发布,不再支持 Node.js 6/8》
《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》
《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》
《JavaScript 已进入第三个时代,未来将何去何从?》
《前端上传前预览文件 image、text、json、video、audio「实践」》
《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》
《推荐13个有用的JavaScript数组技巧「值得收藏」》
《36个工作中常用的JavaScript函数片段「值得收藏」》
《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》
《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》
《JavaScript正则深入以及10个非常有意思的正则实战》
《前端开发规范:命名规范、html规范、css规范、js规范》
《100个原生JavaScript代码片段知识点详细汇总【实践】》
《手把手教你深入巩固JavaScript知识体系【思维导图】》
《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》
《身份证号码的正则表达式及验证详解(JavaScript,Regex)》
《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》
《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《面试中教你绕过关于 JavaScript 作用域的 5 个坑》
作者:大道至简
转发链接:
https://mp.weixin.qq.com/s/0w6aWwpR3MAJnmyLwDnAzA
相关推荐
- 如何设计一个优秀的电子商务产品详情页
-
加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...
- 怎么在JS中使用Ajax进行异步请求?
-
大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...
- 中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革
-
前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...
- 前端监控 SDK 开发分享_前端监控系统 开源
-
一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...
- Ajax 会被 fetch 取代吗?Axios 怎么办?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...
- 前端面试题《AJAX》_前端面试ajax考点汇总
-
1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...
- Ajax 详细介绍_ajax
-
1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...
- 6款可替代dreamweaver的工具_替代powerdesigner的工具
-
dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...
- 我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊
-
接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...
- 福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光
-
时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...
- 2021年超详细的java学习路线总结—纯干货分享
-
本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...
- 不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!
-
Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...
- 2025 年 Python 爬虫四大前沿技术:从异步到 AI
-
作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...
- 最贱超级英雄《死侍》来了!_死侍超燃
-
死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...
- 停止javascript的ajax请求,取消axios请求,取消reactfetch请求
-
一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)
