JavaScript内存泄漏

84 阅读3分钟

在传统的网页开发时无需过多考虑内存管理,通常也不会产生严重的后果。因为当用户点击链接打开新页面或者刷新页面,页面内的信息就会从内存中清理掉。

随着SPA(Single Page Application)应用的增多,迫使我们在编码时需要更多的关注内存。因为如果应用使用的内存逐渐增多会直接影响到网页的性能,甚至导致浏览器标签页崩溃。

这篇文章,我们将研究JavaScript编码导致内存泄漏的场景,提供一些内存管理的建议。

什么是内存泄漏?

我们知道浏览器会把object保存在堆内存中,它们通过索引链可以被访问到。GC(Garbage Collector) 是一个JavaScript引擎的后台进程,它可以鉴别哪些对象是已经处于无用的状态,移除它们,释放占用的内存。

本该被GC回收的变量,如果被其他对象索引,而且可以通过root访问到,这就意味着内存中存在了冗余的内存占用,会导致应用的性能降级,这时也就发生了内存泄漏。

怎样发现内存泄漏?

内存泄漏一般不易察觉和定位,借助浏览器的内建工具可以帮助我们分析是否存在内存泄漏,和导致内存泄漏的原因。

开发者工具

打开开发者工具-Performance选项卡,可以分析当前页面的可视化数据。Chrome 和 Firefox 都有出色的内存分析工具,通过分析快照为开发者提供内存的分配情况。

JS导致内存泄漏的常见情形

未被注意的全局变量

全局变量可以被root访问,不会被GC回收。一些非严格模式下的局部变量可能会变成全局变量,导致内存泄漏。

  • 给没有声明的变量赋值
  • this 指向全局对象
function createGlobalVariables() {
  leaking1 = 'I leak into the global scope'; // assigning value to the undeclared variable
  this.leaking2 = 'I also leak into the global scope'; // 'this' points to the global object
};
createGlobalVariables();
window.leaking1; // 'I leak into the global scope'
window.leaking2; // 'I also leak into the global scope'

如何避免?使用严格模式。

闭包

闭包函数执行完成后,作用域中的变量不会被回收,可能会导致内存泄漏:

function outer() {
  const potentiallyHugeArray = [];

  return function inner() {
    potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
    console.log('Hello');
  };
};
const sayHello = outer(); // contains definition of the function inner

function repeat(fn, num) {
  for (let i = 0; i < num; i++){
    fn();
  }
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray 
 
// now imagine repeat(sayHello, 100000)

定时器

使用setTimeout 或者 setInterval:

function setCallback() {
  const data = {
    counter: 0,
    hugeString: new Array(100000).join('x')
  };

  return function cb() {
    data.counter++; // data object is now part of the callback's scope
    console.log(data.counter);
  }
}

setInterval(setCallback(), 1000); // how do we stop it?

只有当定时器被清理掉的时候,它回调函数内部的data才会被从内存中清理,否则在应用退出前一直会被保留。

如何避免?

function setCallback() {
  // 'unpacking' the data object
  let counter = 0;
  const hugeString = new Array(100000).join('x'); // gets removed when the setCallback returns
  
  return function cb() {
    counter++; // only counter is part of the callback's scope
    console.log(counter);
  }
}

const timerId = setInterval(setCallback(), 1000); // saving the interval ID

// doing something ...

clearInterval(timerId); // stopping the timer i.e. if button pressed

定时器赋值给timerId,使用clearInterval(timerId)手动清理。

Event listeners

addEventListener 也会一直保留在内存中无法回收,直到我们使用了 removeEventListener,或者添加监听事件的DOM被移除。

const hugeString = new Array(100000).join('x');

document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
  doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});

如何避免?

function listener() {
  doSomething(hugeString);
}

document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here

// 或者
document.addEventListener('keyup', function listener() {
  doSomething(hugeString);
}, {once: true}); // listener will be removed after running once

JS内存泄漏总结

鉴别和修复JS内存使用问题是一项有挑战性的任务,编码过程中也要把避免内存泄漏放在第一位。因为这关系到了应用的性能和用户体验。

本文翻译自 《Causes of Memory Leaks in JavaScript and How to Avoid Them》