「前端每日一问(47)」说一下 JS 的内存管理,举一些内存泄漏的例子。

345 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

本题难度:⭐ ⭐

本题类型:综合题、JavaScript

内存管理

不管使用什么编程语言,都会有内存管理的概念。

内存管理包括分配内存,读写内存、释放内存,比如:

let name = '阿林'  // 分配内存
console.log(name) // 读内存
name = 'lin'      // 写内存
name = null       // 释放内存

内存空间可以分为 栈空间堆空间

栈空间用来存储原始数据类型,存储的是固定大小的值。

堆空间用来存储引用类型,内存空间大小并不固定,需按引用情况来进行访问。

看下面的图例,你就能明白 JS 中变量在内存中是以怎样的形式存储的了。

let name = 'lin'
let age = 18
let arr = [1, 2, 3]
let obj = {
  xxx: 123
}

image.png

对于分配内存和读写内存的行为,很多语言都较为一致,但释放内存的行为在不同语言之间有差异。

比如, JavaScript 有垃圾回收机制来自动管理内存的释放,释放内存不需要程序员操心。

但是某些语言,比如 C 或者 C++ 就需要自己去释放内存,给程序员带来了很大的负担,也是很多问题的来源。

但也并不是说 JS 有垃圾回收机制,在释放内存方面就万事大吉了,还是有很多内存泄漏的情况产生。

内存泄露场景举例

什么是内存泄漏?

内存泄漏是指内存中的变量没有使用了,但是没有被释放,存储着就白白浪费了内存。

下面,我列举一些内存泄漏的场景,方便大家理解。

DOM 元素

举个例子,页面中有一个id为 element 的元素。

<div id="element">test element</div>

引用了这个元素,赋值为 element,之后删除了这个元素。

let element = document.getElementById('element')

function remove() {
  element.parentNode.removeChild(element)
}

删除之后再访问这个元素,这个元素还是存在,如下 gif 图所示:

dom 内存泄漏.gif

为了解决这个问题,我们需要在 remove 方法中添加 element = null

function remove() {
  element.parentNode.removeChild(element)
  element = null
}

事件监听

当事件监听器在组件内部挂载了事件处理函数,如果在组件销毁时没有主动清除,这个函数内部引用的变量或函数都不会被垃圾回收机制回收,如果引用变量存储了大量的数据,就白白占用了内存,造成大量内存泄漏。

拿 vue 组件举个例子。

// Home.vue
data() {
  return {
    msg: 'lots of home data'
  }
}
created() {
  window.addEventListener('resize', this.resizeCallback)
}
methods: {
  resizeCallback() {
    console.log('resize --> use', this.msg)
  }
}

在 home 组件里监听了 resize 事件,用到了 home 组件里的数据,路由跳转到 about 组件里,home 组件已经被销毁了,但是 home 组件上被监听的方法和数据依然可以访问,如下 gif 图所示:

事件监听.gif

要解决这个问题也很简单,需要在组件被销毁时移除事件监听:

beforeDestroy() {
  window.removeEventListener('resize', this.resizeCallback)
},

定时器

定时器和事件监听是一样的,一定要记得在合适的时机清除定时器。

data() {
  return {
    msg: 'lots of home data',
    timer: null
  }
}
created() {
  this.timer = setInterval(this.intervalCallback, 500)
}
beforeDestroy() {
  clearInterval(this.timer)
},
methods: {
  intervalCallback() {
    console.log('setInterval --> use', this.msg)
  }
}

全局变量

垃圾回收机制不会回收全局变量,全局变量使用完了之后只有手动清除。

所以尽量不要在全局变量上挂载大量数据,如果迫不得已在全局变量上挂载了很大的数据,也要记得在使用完了之后手动清除。

window.testArr = new Array(1000000).fill('xxx')

// do something

window.testArr = null

一些情况下变量会挂载到 window 上,在实际项目中应使用 lint 工具来避免,比如:

function fn() {
  foo = '123'       // 直接声明变量
  this.bar = 'qwe'  // 把变量赋给 this,this 指向 window
}
fn()

image.png

闭包

闭包的特性就是在函数内产生一些长期驻扎在内存中的变量,如果滥用闭包来存储一些很大的数据,就是白白浪费内存空间。

关于闭包,可以看我的这篇文章:

「前端每日一问(28)」说说你对闭包的理解

结尾

阿林水平有限,文中如果有错误或表达不当的地方,非常欢迎在评论区指出,感谢~

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

你也可以关注《前端每日一问》这个专栏,防止失联哦~

我是阿林,输出洞见技术,再会!

上一篇:

「前端每日一问(46)」随便打开一个网站,统计这个网站用过的标签总数

下一篇:

「前端每日一问(48)」前端如何排查内存泄漏