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

相关推荐
xjt_090114 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农26 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法