JavaScript这几种内存泄露写法,你要小心了

今天我想和你聊聊,前端开发过程中内存泄露的问题。相信你在工作当中遇到过这样的情况,比如,相同的代码在开发环境运行得好好的,到线上运行一段时间后就出现页面运行卡顿,比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。那么,这大概率是发生了内存泄漏。

那么,你可以思考几个问题:

  • 什么是内存泄漏?
  • 为什么会有内存泄漏?
  • 你的哪些写法可能会出现内存泄漏?

好,带着这三个问题,我们来学习今天的内容。

什么是内存泄漏?

首先什么是内存泄漏呢?是放在内存中的变量凭空消失了吗?其实并不是,而是不再使用的内存没有得到释放,导致内存一直在增加。我打个简单的比方:你跟别人要东西,你不停地要,不停地要,当你不需要这些东西的时候你也不还给别人,这就是内存泄漏。

那为什么会出现内存泄漏呢,如果你了解内存的生命周期,一定会知道是哪个环节导致了内存泄漏。不管什么程序语言,它的生命周期一般可以按顺序分为三个部分:

我们先来简单了解下这三个部分都做了什么事情:第一部分是内存分配,当我们创建一个函数或者变量时,必须为它分配一定数量的内存;第二部分是内存的使用,对分配的内存进行读和写操作;第三部分是内存释放阶段,将不需要的内存进行释放。

所有语言的第二部分都是明确的。第一和第三部分在 C 或者 C++ 这类底层语言中是明确的,但在像 JavaScript 这些高级语言中,大部分都是隐含的。JavaScript 是在创建变量时自动进行了分配内存,并且在不使用它们时"自动"释放,释放的过程称为垃圾回收。

问题就出现在这个"自动"的垃圾回收,让前端开发者错误地以为他们可以不关心内存管理。但是实际上浏览器 V8 引擎的垃圾回收只能解决一般问题,它自身有一些局限性。

如果你了解标记 - 清除垃圾回收算法,就会知道,它是首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。

这个算法假定设置一个叫做根的对象,在 JavaScript 里,根就是是全局对象 window。垃圾回收器将定期从全局对象 window 开始,找所有从 window 开始引用的对象,然后找这些对象引用的对象......递归完成后,垃圾回收器将找到所有"可获得"的对象和收集所有"不能获得"的对象,最后将不可获得的对象进行回收。

但是垃圾回收机制会有这样一个问题:假如有一些对象我们已经不需要使用了,但是仍然能被访问到,我们没有对它进行手动清除,那么浏览器引擎的就不会对这个对象回收,当无用的对象越来越多,就会导致内存泄漏。那么哪些写法会导致内存泄漏呢?我总结了 JavaScript 中四类常见的内存泄漏写法和避免方法,给你参考。

哪些写法会导致内存泄漏?

1.未声明/意外的全局变量

第一种,未声明或者意外的全局变量。全局变量的生命周期最长,直到页面关闭前,它都存活着,所以全局变量上的内存一直都不会被回收。所以当我们写代码的时候如果全局变量使用不当,没有及时回收,或者拼写错误将某个变量挂载到全局变量时,就会发生内存泄漏了。

js 复制代码
var a = '这是一个全局变量';
function test(){
    b = '变量b'; //b 成为一个全局变量,不会被回收
}

这段代码中,test 函数中定义了一个变量 b,没有使用 var 或者 let 变量进行声明,这时 b 会成为全局变量,test 执行后变量 b 不会被回收。解决方式也比较简单,在 JavaScript 文件中添加'use strict',开启严格模式,这个时候就不能使用 b 这个意外的全局变量了,开发时就会在浏览器控制台报错,避免这种情况发生。

js 复制代码
var a = '这是一个全局变量';
function test(){
    b = '变量b'; //b 成为一个全局变量,不会被回收
}

2.遗忘的定时器

第二类常见的内存泄漏是定时器 setTimeout 和 setInterval,它的生命周期是由浏览器专门的线程来维护的,所以当在某个页面使用了定时器,并且这个页面销毁时,如果你没有手动去释放清理这些定时器,那么这些定时器还是存活着的。

来看一个示例,这段代码通过定时器注册了一个回调函数,该回调函数内又持有当前页面 ID 为 Node 的 DOM 元素。当页面销毁之后,由于定时器持有该页面部分引用而造成定时器没有被回收,进而导致定时器内部的数据 someData 也无法被回收,就导致了内存泄漏。

js 复制代码
var someData = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someData));
    }
}, 1000);

解决办法是,在不使用定时器的时候将定时器取消,setInterval 设置一个 ID,然后就可以通过 clearInterval(id) 进行取消了。

js 复制代码
var someData = getData();
var intervalId = setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someData));
    }
}, 1000);
// 在不使用的时候进行清除
clearInterval(intervalId)

3.事件绑定

第三种由"事件绑定"导致的内存泄漏也非常常见,一般是由于事件响应函数没有及时移除,导致重复绑定或者 DOM 元素已经移除后未处理事件响应函数造成的,例如这段 React 代码:

js 复制代码
class Demo extends React.Component {
  componentDidMount() {
    window.addEventListener('resize', function() {
      // do something
    });
  }
  render() {
    return <div>test component</div>;
  }
}

组件在挂载的时候监听了 resize 事件,但是在组件移除的时候没有处理相应函数,假如 的挂载和移除非常频繁,那么就会在 window 上绑定很多无用的事件监听函数,最终导致内存泄漏。

那怎么解决呢?我们可以通过在组件卸载 componentWillUmout 的时候移除监听事件来避免这个问题:

js 复制代码
class Demo extends React.Component {
  componentDidMount() {
    window.addEventListener('resize', this.handleResize);
  }
  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
  }
  handleResize() {
    // handle resize
  }
  render() {
    return <div>test component</div>;
  }
}

4.闭包

最后一类比较重要,也是我们在开发过程中经常使用到的,那就是闭包。这里先声明:闭包本身没有错,不会引起内存泄漏,而是我们使用错误导致的。

我们都知道,闭包有两个主要作用,一是延伸变量作用域范围,读取函数内部的变量。二是让这些变量的值始终保持在内存中。简单理解就是,一个作用域可以访问另外一个函数内部的局部变量。

那么,为什么会说闭包可能会导致内存泄漏呢?函数本身会持有它定义时所在的词法环境的引用,但通常情况下,使用完函数后,该函数所申请的内存都会被回收了。但当函数内再返回一个函数时,由于返回的函数持有外部函数的词法环境,而返回的函数又被其他生命周期东西所持有,导致外部函数虽然执行完了,但内存却无法被回收。

我们举个例子来看一下闭包,这个例子中函数 f1 里面返回了一个函数 f2,f2 中使用了 f1 函数中的变量 n,这样就形成了闭包。你可以想一下 console.log 的内容应该是什么?好,答案是在执行完 f1 函数后,第一次调用 result 结果返回 1000,第二次调用结果返回了 1001。这说明了什么呢?说明变量 n 在函数执行完并没有被销毁,而是继续留在了内存中。

js 复制代码
function f1() {
  var n = 999;
  // 一个闭包
  function f2() {
    n++; // f2 作为内部函数,有权访问父级函数作用域 f1 中的变量
    console.log(n);
  }
  return f2;
}
var result = f1();
result(); // 1000
result(); // 1001

正常来说,闭包并不是内存泄漏,因为这种持有外部函数词法环境本就是闭包的特性,就是为了让这块内存不被回收,因为可能在未来还需要用到,但这无疑会造成内存的消耗,所以,我们不应该滥用闭包。

总结

今天的分享到这里就结束了,最后我们来回顾一下今天讲的内容。内存泄漏发生在 JavaScript 内存自动回收阶段,浏览器引擎在"自动"回收阶段使用的是标记清除算法,可以将"不可获得"的对象进行回收,如果我们编写代码的时候对一些全局变量处理不当,定时器和事件绑定没有及时清除,或者闭包使用不当,就会引起内存泄漏问题。所以为了避免内存泄漏,最重要对一点就是养成良好对编程习惯,比如内存分配后,一定要注意写好内存释放的代码,有借有还,才能高效运转,再借不难。

相关推荐
Michael.Scofield2 分钟前
vue: router基础用法
前端·javascript·vue.js
excel5 分钟前
webpack 模块 第 五 节
前端
excel14 分钟前
webpack 模块 第 四 节
前端
好_快23 分钟前
Lodash源码阅读-take
前端·javascript·源码阅读
好_快24 分钟前
Lodash源码阅读-takeRight
前端·javascript·源码阅读
好_快25 分钟前
Lodash源码阅读-takeRightWhile
前端·javascript·源码阅读
烂蜻蜓26 分钟前
在 HTML5 中使用 MathML 展示数学公式
前端·html·html5
好_快28 分钟前
Lodash源码阅读-takeWhile
前端·javascript·源码阅读
风中飘爻1 小时前
JavaScript:BOM编程
开发语言·javascript·ecmascript
恋猫de小郭2 小时前
Android Studio Cloud 正式上线,不只是 Android,随时随地改 bug
android·前端·flutter