js面试之闭包和内存泄漏

1,621 阅读3分钟

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

闭包是什么?对页面有什么影响?

  • 闭包有三个特性
    • 函数嵌套函数
    • 函数内部可以引用外部的参数与变量
    • 参数和变量不会被垃圾回收机制回收
  • 闭包的缺点
    • 常驻内存,增大内存使用量,使用不当会造成内存泄漏
  • 闭包的优点
    • 变量长期驻扎在内存中
    • 避免全局变量的污染
    • 私有成员的产生

为什么要使用闭包?

  • 设计私有方法和变量
  • 避免全局变量污染
  • 希望变量长期驻扎在内存中

应用

  • 模块化代码,减少全局变量的污染
// 模块化代码,减少全部变量的污染
var abc = (function () {
  var a = 1
  return function () {
    a++
    alert(a)
  }
})()
abc() // 2
abc() // 3
  • 私有成员的存在(不想要方法外面访问的成员)
// 私有成员
var a = function () {
  var b = 1
  function cc() {
    console.log(b++)
  }
  function dd() {
    console.log(b++)
  }
  return {
    c:cc,
    d:dd
  }
}
var m = a()
m.c() // 1
m.d() // 2
  • 使用匿名函数实现累加
// 使用匿名函数使用累加
function box () {
  var num = 1
  return function () {
    num++
    console.log(num)
  }
}
var add = box()
add() // 2
add() // 3
  • 在循环中直接找到对应元素的索引
// 在循环中直接找到对应元素的索引
// 方法1
window.onload = function () {
  var li = document.getElementsByTagName('li')
  for (var i = 0; i < li.length; i++) {
      li[i].index = i
      li[i].onclick = function (){
        alert(this.index)
      }
  }
}
// 方法2
window.onload = function () {
  var li = document.getElementsByTagName('li')
  for (var i = 0; i < li.length; i++) {
      li[i].onclick = (function (a){
        return function () {
          alert(a)
        }
      })(i)
  }
}

增加

  • 其实你写的每一个js函数都是闭包,一个js函数的顶层作用域就是window对象js的执行环境本身就是一个scope(浏览器的window/node的global),我们通常称之为全局作用域。
  • 每个函数,不论多深,都可以认为是全局scope的子作用域,可以理解为闭包。

哪些操作会造成内存泄漏?

  • 内存泄露:不再用到的内存,没有及时释放
  • 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为0(没有其他对象引用过该对象),或该对象的惟一引用是循环的,那么该对象的内存即可回收

操作:

  • 意外的全局变量:全局变量引用、变量未申明。当全局变量使用不当,没有及时回收(手动赋值 null),或者拼写错误等将某个变量挂载到全局变量时,也就发生内存泄漏了
  • 被遗忘的计时器或回调函数:
    • DOM元素的生命周期正常是取决于: 是否挂载在 DOM 树上,当从 DOM 树上移除时,也就可以被销毁回收了。
    • 但如果某个 DOM 元素,在 js 中也持有它的引用时,那么它的生命周期就由 js 和是否在 DOM 树上两者决定了,记得移除时,两个地方都需要去清理才能正常回收它
    • setTiemout 也会有同样的问题,所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout来清除
// 也就是调用了 clearInterval。如果没有被 clear 掉的话,就会造成内存泄漏。不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。所以在上例中,someResource 就没法被回收。
// 获取数据
let someResource = getData()
setInterval(() => {
  const node = document.getElementById('Node')
  if(node) {
    node.innerHTML = JSON.stringify(someResource))
  }
}, 1000)
  • 脱离DOM的引用:使用变量缓存DOM节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。
<div id="root">
<ul id="ul">
  <li></li>
  <li></li>
  <li id="li3"></li>
  <li></li>
</ul>
</div>
<script>
  let root = document.querySelector('#root')
  let ul = document.querySelector('#ul')
  let li3 = document.querySelector('#li3')
  
  // 由于ul变量存在,整个ul及其子元素都不能GC
  root.removeChild(ul)
  
  // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
  ul = null
  
  // 已无变量引用,此时可以GC
  li3 = null
</script>
  • 闭包: 不正当的使用闭包可能会造成内存泄漏,记得最后将对象置空
// 1.闭包
function fn () {
  var num = new Array(10).fill('ok')
  return function () {
    console.log(num)
  }
}

let fn1 = fn()
fn1()
// 函数调用后,将外部的引用关系置空
// 就可以释放函数和外部函数的num变量
fn1 = null

拓展

  • 垃圾回收的必要性(引自《JavaScript权威指南(第四版)》)

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

  • JS垃圾回收的机制:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。
  • 不使用的对象写成null

参考文章