0

0

常见的JavaScript内存泄露

小云云

小云云

发布时间:2017-12-05 16:31:45

|

1894人浏览过

|

来源于php中文网

原创

什么是内存泄露

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
内存泄漏通常情况下只能由获得程序源代码的程序员才能分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。
————wikipedia

意外的全局变量

JavaScript对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete删除)。如果在浏览器中,全局对象就是window对象。

如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。

<span style="font-size: 14px;">function foo(arg) {<br/>    bar = "this is a hidden global variable with a large of data";<br/>}<br/></span>

等同于:

<span style="font-size: 14px;">function foo(arg) {<br/>    window.bar = "this is an explicit global variable with a large of data";<br/>}<br/></span>

另外,通过this创建意外的全局变量:

立即学习Java免费学习笔记(深入)”;

<span style="font-size: 14px;">function foo() {<br/>    this.variable = "potential accidental global";<br/>}<br/><br/>// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'<br/>foo();<br/></span>

解决方法:

在JavaScript文件中添加'use strict',开启严格模式,可以有效地避免上述问题。

<span style="font-size: 14px;">function foo(arg) {<br/>    "use strict" // 在foo函数作用域内开启严格模式<br/>    bar = "this is an explicit global variable with a large of data";// 报错:因为bar还没有被声明<br/>}<br/></span>

如果需要在一个函数中使用全局变量,可以像如下代码所示,在window上明确声明:

<span style="font-size: 14px;">function foo(arg) {<br/>    window.bar = "this is a explicit global variable with a large of data";<br/>}<br/></span>

这样不仅可读性高,而且后期维护也方便

谈到全局变量,需要注意那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。全局变量也常用来做cache,一般cache都是为了性能优化才用到的,为了性能,最好对cache的大小做个上限限制。因为cache是不能被回收的,越高cache会导致越高的内存消耗。

console.log

console.log:向web开发控制台打印一条消息,常用来在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉console.log语句,这可能造成内存泄露。

在传递给console.log的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。

实例------>demos/log.html

<span style="font-size: 14px;"><!DOCTYPE html><br/><html lang="en"><br/><br/><head><br/>  <meta charset="UTF-8"><br/>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br/>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br/>  <title>Leaker</title><br/></head><br/><br/><body><br/>  <input type="button" value="click"><br/>  <script><br/>    !function () {<br/>      function Leaker() {<br/>        this.init();<br/>      };<br/>      Leaker.prototype = {<br/>        init: function () {<br/>          this.name = (Array(100000)).join('*');<br/>          console.log("Leaking an object %o: %o", (new Date()), this);// this对象不能被回收<br/>        },<br/><br/>        destroy: function () {<br/>          // do something....<br/>        }<br/>      };<br/>      document.querySelector('input').addEventListener('click', function () {<br/>        new Leaker();<br/>      }, false);<br/>    }()<br/>  </script><br/></body><br/><br/></html><br/></span>

这里结合Chrome的Devtools–>Performance做一些分析,操作步骤如下:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 开启【Performance】项的记录

  2. 执行一次CG,创建基准参考线

  3. 连续单击【click】按钮三次,新建三个Leaker对象

  4. 执行一次CG

  5. 停止记录

常见的JavaScript内存泄露

可以看出【JS Heap】线最后没有降回到基准参考线的位置,显然存在没有被回收的内存。如果将代码修改为:

<span style="font-size: 14px;">    !function () {<br/>      function Leaker() {<br/>        this.init();<br/>      };<br/>      Leaker.prototype = {<br/>        init: function () {<br/>          this.name = (Array(100000)).join('*');<br/>        },<br/><br/>        destroy: function () {<br/>          // do something....<br/>        }<br/>      };<br/>      document.querySelector('input').addEventListener('click', function () {<br/>        new Leaker();<br/>      }, false);<br/>    }()<br/></span>

去掉console.log("Leaking an object %o: %o", (new Date()), this);语句。重复上述的操作步骤,分析结果如下:

常见的JavaScript内存泄露

从对比分析结果可知,console.log打印的对象是不会被垃圾回收器回收的。因此最好不要在页面中console.log任何大对象,这样可能会影响页面的整体性能,特别在生产环境中。除了console.log外,另外还有console.dir、console.error、console.warn等都存在类似的问题,这些细节需要特别的关注。

closures(闭包)

当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量,这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。

<span style="font-size: 14px;">function foo(message) {<br/>    function closure() {<br/>        console.log(message)<br/>    };<br/>    return closure;<br/>}<br/><br/>// 使用<br/>var bar = foo("hello closure!");<br/>bar()// 返回 'hello closure!'<br/></span>

在函数foo内创建的函数closure对象是不能被回收掉的,因为它被全局变量bar引用,处于一直可访问状态。通过执行bar()可以打印出hello closure!。如果想释放掉可以将bar = null即可。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

实例------>demos/closures.html

<span style="font-size: 14px;"><!DOCTYPE html><br/><html lang="en"><br/><br/><head><br/>  <meta charset="UTF-8"><br/>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br/>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br/>  <title>Closure</title><br/></head><br/><br/><body><br/>  <p>不断单击【click】按钮</p><br/>  <button id="click_button">Click</button><br/>  <script><br/>    function f() {<br/>      var str = Array(10000).join('#');<br/>      var foo = {<br/>        name: 'foo'<br/>      }<br/>      function unused() {<br/>        var message = 'it is only a test message';<br/>        str = 'unused: ' + str;<br/>      }<br/>      function getData() {<br/>        return 'data';<br/>      }<br/>      return getData;<br/>    }<br/><br/>    var list = [];<br/>    <br/>    document.querySelector('#click_button').addEventListener('click', function () {<br/>      list.push(f());<br/>    }, false);<br/>  </script><br/></body><br/><br/></html><br/></span>

这里结合Chrome的Devtools->Memory工具进行分析,操作步骤如下:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 选中【Record allocation timeline】选项

  2. 执行一次CG

  3. 单击【start】按钮开始记录堆分析

  4. 连续单击【click】按钮十多次

  5. 停止记录堆分析

常见的JavaScript内存泄露

上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:

常见的JavaScript内存泄露

查看对象的详细信息:

常见的JavaScript内存泄露

CreateWise AI
CreateWise AI

为播客创作者设计的AI创作工具,AI自动去口癖、提交亮点和生成Show notes、标题等

下载

从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个str字段。而str字段并没有在返回getData()中使用过。为什么会存在在作用域中,按理应该被GC回收掉, why

原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:

<span style="font-size: 14px;">    function f() {<br/>      var str = Array(10000).join('#');<br/>      var foo = {<br/>        name: 'foo'<br/>      }<br/>      function unused() {<br/>        var message = 'it is only a test message';<br/>        // str = 'unused: ' + str; //删除该条语句<br/>      }<br/>      function getData() {<br/>        return 'data';<br/>      }<br/>      return getData;<br/>    }<br/><br/>    var list = [];<br/>    <br/>    document.querySelector('#click_button').addEventListener('click', function () {<br/>      list.push(f());<br/>    }, false);<br/></span>

getData()和unused()内部函数共享f函数对应的变量对象,因为unused()内部函数访问了f作用域内str变量,所以str字段存在于f变量对象中。加上getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的f变量对象存在于闭包函数的作用域链中。这里只要将函数unused中str = 'unused: ' + str;语句删除便可解决问题。

常见的JavaScript内存泄露

查看一下闭包信息:

常见的JavaScript内存泄露

DOM泄露

在JavaScript中,DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。如Chrome浏览器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。了解更多ℹ️

常见的JavaScript内存泄露

为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。

实例------>demos/dom.html

<span style="font-size: 14px;"><!DOCTYPE html><br/><html lang="en"><br/><head><br/>  <meta charset="UTF-8"><br/>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br/>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br/>  <title>Dom-Leakage</title><br/></head><br/><body><br/>  <input type="button" value="remove" class="remove"><br/>  <input type="button" value="add" class="add"><br/><br/>  <p class="container"><br/>    <pre class="wrapper"></pre><br/>  </p><br/>  <script><br/>    // 因为要多次用到<pre>节点,将其缓存到本地变量wrapper中,<br/>    var wrapper = document.querySelector('.wrapper');<br/>    var counter = 0;<br/><br/>    document.querySelector('.remove').addEventListener('click', function () {<br/>      document.querySelector('.container').removeChild(wrapper);<br/>    }, false);<br/><br/>    document.querySelector('.add').addEventListener('click', function () {<br/>      wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));<br/>    }, false);<br/>  </script><br/></body><br/></html><br/></span>

这里结合Chrome浏览器的Devtools–>Performance做一些分析,操作步骤如下:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 开启【Performance】项的记录

  2. 执行一次CG,创建基准参考线

  3. 连续单击【add】按钮6次,增加6个文本节点到pre元素中

  4. 单击【remove】按钮,删除刚增加6个文本节点和pre元元素

  5. 执行一次CG

  6. 停止记录堆分析

常见的JavaScript内存泄露

从分析结果图可知,虽然6次add操作增加6个Node,但是remove操作并没有让Nodes节点数下降,即remove操作失败。尽管还主动执行了一次CG操作,Nodes曲线也没有下降。因此可以断定内存泄露了!那问题来了,如何去查找问题的原因呢?这里可以通过Chrome浏览器的Devtools–>Memory进行诊断分析,执行如下操作步骤:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 选中【Take heap snapshot】选项

  2. 连续单击【add】按钮6次,增加6个文本节点到pre元素中

  3. 单击【Take snapshot】按钮,执行一次堆快照

  4. 单击【remove】按钮,删除刚增加6个文本节点和pre元元素

  5. 单击【Take snapshot】按钮,执行一次堆快照

  6. 选中生成的第二个快照报告,并将视图由"Summary"切换到"Comparison"对比模式,在[class filter]过滤输入框中输入关键字:Detached

常见的JavaScript内存泄露

从分析结果图可知,导致整个pre元素和6个文本节点无法别回收的原因是:代码中存在全局变量wrapper对pre元素的引用。知道了产生的问题原因,便可对症下药了。对代码做如下就修改:

<span style="font-size: 14px;">    // 因为要多次用到<pre>节点,将其缓存到本地变量wrapper中,<br/>    var wrapper = document.querySelector('.wrapper');<br/>    var counter = 0;<br/><br/>    document.querySelector('.remove').addEventListener('click', function () {<br/>      document.querySelector('.container').removeChild(wrapper);<br/>      wrapper = null;//在执行删除操作时,将wrapper对pre节点的引用释放掉<br/>    }, false);<br/><br/>    document.querySelector('.add').addEventListener('click', function () {<br/>      wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));<br/>    }, false);<br/></span>

在执行删除操作时,将wrapper对pre节点的引用释放掉,即在删除逻辑中增加wrapper = null;语句。再次在Devtools–>Performance中重复上述操作:

常见的JavaScript内存泄露

小试牛刀------>demos/dom_practice.html

再来看看网上的一个实例,代码如下:

<span style="font-size: 14px;"><!DOCTYPE html><br/><html lang="en"><br/><head><br/>  <meta charset="UTF-8"><br/>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br/>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br/>  <title>Practice</title><br/></head><br/><body><br/>  <p id="refA"><ul><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#" id="refB"></a></li></ul></p><br/>  <p></p><br/>  <p></p><br/><br/>  <script><br/>    var refA = document.getElementById('refA');<br/>    var refB = document.getElementById('refB');<br/>    document.body.removeChild(refA);<br/><br/>    // #refA不能GC回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。<br/>    refA = null;<br/><br/>    // 还存在变量refB对#refB的间接引用(refB引用了#refB,而#refB属于#refA)。将变量refB对#refB的引用释放,refA就可以被GC回收。<br/>    refB = null;<br/>  </script><br/></body><br/></html><br/></span>

整个过程如下图所演示:

常见的JavaScript内存泄露

有兴趣的同学可以使用Chrome的Devtools工具,验证一下分析结果,实践很重要~~~

timers

在JavaScript常用setInterval()来实现一些动画效果。当然也可以使用链式setTimeout()调用模式来实现:

<span style="font-size: 14px;">setTimeout(function() {<br/>  // do something. . . .<br/>  setTimeout(arguments.callee, interval);<br/>}, interval);<br/></span>

如果在不需要setInterval()时,没有通过clearInterval()方法移除,那么setInterval()会不停地调用函数,直到调用clearInterval()或窗口关闭。如果链式setTimeout()调用模式没有给出终止逻辑,也会一直运行下去。因此再不需要重复定时器时,确保对定时器进行清除,避免占用系统资源。另外,在使用setInterval()和setTimeout()来实现动画时,无法确保定时器按照指定的时间间隔来执行动画。为了能在JavaScript中创建出平滑流畅的动画,浏览器为JavaScript动画添加了一个新API-requestAnimationFrame()。关于setInterval、setTimeout与requestAnimationFrame实现动画上的区别➹猛击

相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

46

2026.03.12

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

178

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

51

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

92

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

102

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

227

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

532

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

171

2026.03.04

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.4万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号