JavaScript 内存泄漏及如何避免

前言

在过去,内存泄漏并没有为 Web 开发人员带来巨大的问题。页面保持着相对简单,并且在页面之间的跳转时可以释放内存资源,即便还存在内存泄露,那也是小到可以被忽略。

现在,新的 Web 应用达到更高的水准,页面可能运行数小时而不跳转,通过 Web 服务动态检索和更新页面。JavaScript 语言特性也被发挥到极致,通过复杂的事件绑定、面向对象和闭包等特性构成了整个 Web 应用。面对这些变化,内存泄露问题变得越来越突出,尤其是之前那些通过刷新(导航)隐藏的内存泄露问题。

内存泄漏是每个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其他应用问题。

一、什么是内存泄漏?

内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。

二、JavaScript 内存管理

JavaScript 是一种垃圾回收语言。垃圾回收语言通过周期性地检查先前分配的内存是否可达,帮助开发者管理内存。换言之,垃圾回收语言减轻了“内存仍可用”及“内存仍可达”的问题。两者的区别是微妙而重要的:仅有开发者了解哪些内存在将来仍会使用,而不可达内存通过算法确定和标记,适时被操作系统回收。垃圾回收语言的内存泄漏主因是不需要的引用。理解它之前,还需了解垃圾回收语言如何辨别内存的可达与不可达。

三、Mark-and-sweep

大部分垃圾回收语言用的算法称之为 Mark-and-sweep 。算法由以下几步组成:

  1. 垃圾回收器创建了一个“roots”列表。Roots 通常是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被当作 root 。window 对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
  2. 所有的 roots 被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从 root 开始的所有对象如果是可达的,它就不被当作垃圾。
  3. 所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。

现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。

不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍被留在激活的 root 树中。在 JavaScript 中,不需要的引用是保留在代码中的变量,它不再需要,却指向一块本该被释放的内存。有些人认为这是开发者的错误。

为了理解 JavaScript 中最常见的内存泄漏,我们需要了解哪种方式的引用容易被遗忘。

四、几种常见 JavaScript 内存泄漏

【1】意外的全局变量

JavaScript 处理未定义变量的方式比较宽松,未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是Window

function foo(arg) {
  bar = "this is a hidden global variable";
}

// 真相是函数foo内部忘记使用var,意外创建了一个全局变量bar,此例泄漏了一个简单的字符串

function foo(arg) {
  window.bar = "this is an explicit global variable";
}

另一种意外的全局变量可能由 this 创建

function foo() {
  this.variable = "potential accidental global";
}

foo();

// 调用foo(),this指向了全局对象(window)

解决办法:在 JavaScript 文件头部加上 'use strict',使用严格模式解析JavaScript避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。

尽管我们讨论了一些意外的全局变量,但是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或重新分配)。尤其当全局变量用于临时存储和处理大量信息时,需要多加小心。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。

【2】循环引用

function func() {  
    let A = {};  
    let B = {};  
  
    A.a = B; // A 引用 B
    B.a = A; // B 引用 A
}

对于纯粹的 ECMAScript 对象而言,只要没有其他对象引用对象 A、B,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并回收处理。但是,在 Internet Explorer 中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会发现它们之间的循环关系与系统中的其他对象是隔离的并释放它们。最终它们将被保留在内存中,直到浏览器关闭。

解决办法:A和B都设为null

【3】被遗忘的定时器或延时器

在 JavaScript 中使用 setInterval和setTimeout 很平常,但是使用完之后通常忘记清理

let result = getData();
setInterval(function () {
    let node = document.getElementById('Node');
    if (node) {
        // 处理 node 和 result
        node.innerHTML = JSON.stringify(result);
    }
}, 1000)

setInterval、setTimeout 中的 this 指向的是window对象,所以内部定义的变量也挂载到了全局;if 内引用了 result 变量,如果没有清除 setInterval的话 result 也得不到释放;同理其实 setTimeout 也一样。

解决办法:用完后记得去clearInterval、clearTimeout

【4】闭包

闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。

function bindEvent() {
    let obj = document.createElement("XXX");
    obj.onclick = function () {
        // do something 
    }
}

闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包

解决办法:

方法一:将事件处理函数定义在外部,解除闭包

方法二:在定义事件处理函数的外部函数中,删除对dom的引用,《JavaScript权威指南》中介绍过,闭包中,作用域中没用的属性可以删除,以减少内存消耗。

// 方法一
function bindEvent() {
    let obj = document.createElement("XXX");
    obj.onclick = onclickHandler;
}
function onclickHandler() {
    //do something 
}


// 方法二
function bindEvent() {
    let obj = document.createElement("XXX");
    obj.onclick = function () {
        //do something 
    }
    obj = null;
}

【5】DOM引起的内存泄露

★当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。

let btn = document.getElementById("myBtn");
btn.onclick = function () {
    document.getElementById("myDiv").innerHTML = "XXX";
}

解决办法:

方法一:手动移除事件

方法二:采用事件委托

// 手动移除事件
let btn = document.getElementById("myBtn");
btn.onclick = function () {
    btn.onclick = null;
    document.getElementById("myDiv").innerHTML = "XXX";
}

// 采用事件委托
document.onclick = function (event) {
    event = event || window.event;
    if (event.target.id == "myBtn") {
        document.getElementById("myDiv").innerHTML = "XXX";
    }
}

★未清除DOM引用

let myDiv = document.getElementById('myDiv');
document.body.removeChild(myDiv);

myDiv不能回收,因为存在变量myDiv对它的引用

解决办法:myDiv = null

★DOM对象添加的属性是一个对象的引用

let MyObject = {}; 
document.getElementById('myDiv').myProp = MyObject;

解决办法:在页面 onunload 事件中释放 document.getElementById('myDiv').myProp = null

【6】自动类型转换

let s = 'xxx';
console.log(s.length);

s本身是一个string而非object,它没有length属性,所以当访问length时,JS引擎会自动创建一个临时String对象封装s,而这个对象一定会泄露。

解封办法:记得所有值类型做运算之前先显式转换一下

let s = 'xxx';
console.log(new String(s).length);

文章每周持续更新,可以微信搜索「 前端大集锦 」第一时间阅读,回复【视频】【书籍】领取200G视频资料和30本PDF书籍资料

猜你喜欢

转载自blog.csdn.net/qq_38128179/article/details/107101222