深入JavaScript教你内存泄漏如何防范( 二 )

上面例子中的数组 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 Collectorconsole.log(mapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321") // 依然在缓存里


推荐阅读