加入收藏 | 设为首页 | 会员中心 | 我要投稿 晋中站长网 (https://www.0354zz.com/)- 科技、容器安全、数据加密、云日志、云数据迁移!
当前位置: 首页 > 教程 > 正文

JavaScript闭包用多会不会造成内存泄露

发布时间:2023-07-28 10:00:24 所属栏目:教程 来源:未知
导读:   给大家分享一下JavaScript闭包用多会造成内存泄露吗的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章
  给大家分享一下JavaScript闭包用多会造成内存泄露吗的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
 
  场景思考
 
  以下案例: A 页面引入了一个 debounce 防抖函数,跳转到 B 页面后,该防抖函数中闭包所占的内存会被 gc 回收吗?
 
  该案例中,通过变异版的防抖函数来演示闭包的内存回收,此函数中引用了一个内存很大的对象 info(42M的内存),便于明显地对比内存的前后变化
 
  注:可以使用 Chrome 的 Memory 工具查看页面的内存大小:

  场景步骤:
 
  1) util.js 中定义了 debounce 防抖函数
 
  // util.js`let info = {  arr: new Array(10 * 1024 * 1024).fill(1),  timer: null};export const debounce = (fn, time) => {  return function (...args) {
 
      info.timer && clearTimeout(info.timer);
 
      info.timer = setTimeout(() => {
 
        fn.apply(this, args);
 
      }, time);
 
    };
 
  };
 
  2) A 页面中引入并使用该防抖函数
 
  import { debounce } from './util';mounted() {    this.debounceFn = debounce(() => {      console.log('1');
 
      }, 1000)
 
  }
 
  抓取 A 页面内存: 57.1M
 
  3) 从 A 页面跳转到 B 页面,B 页面中没有引入该 debounce 函数
 
  问题: 从 A 跳转到 B 后,该函数所占的内存会被释放掉吗?
 
  此时,抓取 B 页面内存: 58.1M
 
  刷新 B 页面,该页面的原始内存为: 16.1M
 
  结论: 前后对比发现,从 A 跳转到 B 后,B 页面内存增大了 42M,证明该防抖函数所占的内存没有被释放掉,即造成了内存泄露
 
  为什么会这样呢? 按理说跳转 B 页面后,A 页面的组件都被销毁掉了,那么 A 页面所占的内存应该都被释放掉了啊?
 
  我们继续对比测试
 
  4) 如果把 info 对象放到 debounce 函数内部,从 A 跳转到 B 后,该防抖函数所占的内存会被释放掉吗?
 
  // util.js`export const debounce = (fn, time) => { let info = {  arr: new Array(10 * 1024 * 1024).fill(1),  timer: null
 
   };  return function (...args) {
 
      info.timer && clearTimeout(info.timer);
 
      info.timer = setTimeout(() => {
 
        fn.apply(this, args);
 
      }, time);
 
    };
 
  };
 
  按照步骤 4 的操作,重新从 A 跳转到 B 后,B 页面抓取内存为16.1M,证明该函数所占的内存被释放掉了
 
  为什么只是改变了 info 的位置,会引起内存的前后变化?
 
  要搞懂这个问题,需要理解闭包的内存回收机制
 
  闭包简介
 
  闭包:一个函数内部有外部变量的引用,比如函数嵌套函数时,内层函数引用了外层函数作用域下的变量,就形成了闭包。最常见的场景为:函数作为一个函数的参数,或函数作为一个函数的返回值时
 
  闭包示例:
 
  function fn() {
 
    let num = 1;
 
    return function f1() {
 
      console.log(num);
 
    };}
 
  let a = fn();a();
 
  上面代码中,a 引用了 fn 函数返回的 f1 函数,f1 函数中引入了内部变量 num,导致变量 num 滞留在内存中
 
  打断点调试一下
 
  展开函数 f 的 Scope(作用域的意思)选项,会发现有 Local 局部作用域、Closure 闭包、Global 全局作用域等值,展开 Closure,会发现该闭包被访问的变量是 num,包含 num 的函数为 fn
 
  总结来说,函数 f 的作用域中,访问到了fn 函数中的 num 这个局部变量,从而形成了闭包
 
  所以,如果真正理解好闭包,需要先了解闭包的内存引用,并且要先搞明白这几个知识点:
 
  函数作用域链
 
  执行上下文
 
  变量对象、活动对象
 
  函数的内存表示
 
  先从最简单的代码入手,看下变量是如何在内存中定义的
 
  let a = '小马哥'
 
  这样一段代码,在内存里表示如下
 
  JavaScript闭包用多会造成内存泄露吗
 
  在全局环境 window 下,定义了一个变量 a,并给 a 赋值了一个字符串,箭头表示引用
 
  再定义一个函数
 
  let a = '小马哥'function fn() {  let num = 1}
 
  内存结构如下:
 
  JavaScript闭包用多会造成内存泄露吗
 
  特别注意的是,fn 函数中有一个 [[scopes]] 属性,表示该函数的作用域链,该函数作用域指向全局作用域(浏览器环境就是 window),函数的作用域是理解闭包的关键点之一
 
  请谨记:函数的作用域链是在创建时就确定了,JS 引擎会创建函数时,在该对象上添加一个名叫作用域链的属性,该属性包含着当前函数的作用域以及父作用域,一直到全局作用域
 
  函数在执行时,JS 引擎会创建执行上下文,该执行上下文会包含函数的作用域链(上图中红色的线),其次包含函数内部定义的变量、参数等。在执行时,会首先查找当前作用域下的变量,如果找不到,就会沿着作用域链中查找,一直到全局作用域
 
  垃圾回收机制浅析
 
  现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数
 
  这里重点介绍 "引用计数"(reference counting),JS 引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

  左下角的两个值,没有任何引用,所以可以释放
 
  如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏
 
  判断一个对象是否会被垃圾回收的标准: 从全局对象 window 开始,顺着引用表能找到的都不是内存垃圾,不会被回收掉。只有那些找不到的对象才是内存垃圾,才会在适当的时机被 gc 回收
 
  分析内存泄露的原因
 
  回到最开始的场景,当 info 在 debounce 函数外部时,为什么会造成内存泄露?
 
  进行断点调试

  展开 debounce 函数的 Scope选项,发现有两个 Closure 闭包对象,第一个 Closure 中包含了 info 对象,第二个 Closure 闭包对象,属于 util.js 这个模块
 
  内存结构如下:
 
  当从 A 页面切换到 B 页面时,A 页面被销毁,只是销毁了 debounce 函数当前的作用域,但是 util.js 这个模块的闭包却没有被销毁,从 window 对象上沿着引用表依然可以查找到 info 对象,最终造成了内存泄露
 
  当 info 在 debounce 函数内部时,进行断点调试
 
  其内存结构如下:

  当从 A 页面切换到 B 页面时,A 页面被销毁,同时会销毁 debounce 函数当前的作用域,从 window 对象上沿着引用表查找不到 info 对象,info 对象会被 gc 回收
 
  闭包内存的释放方式
 
  1、手动释放(需要避免的情况)
 
  如果将闭包引用的变量定义在模块中,这种会造成内存泄露,需要手动释放,如下所示,其他模块需要调用 clearInfo 方法,来释放 info 对象
 
  可以说这种闭包的写法是错误的 (不推荐), 因为开发者需要非常小心,否则稍有不慎就会造成内存泄露,我们总是希望可以通过 gc 自动回收,避免人为干涉
 
  let info = {  arr: new Array(10 * 1024 * 1024).fill(1),  timer: null};export const debounce = (fn, time) => {  return function (...args) {
 
      info.timer && clearTimeout(info.timer);
 
      info.timer = setTimeout(() => {
 
        fn.apply(this, args);
 
      }, time);
 
    };
 
  };export const clearInfo = () => {
 
    info = null;
 
  };
 
  2、自动释放(大多数的场景)
 
  闭包引用的变量定义在函数中,这样随着外部引用的销毁,该闭包就会被 gc 自动回收 (推荐),无需人工干涉
 
  export const debounce = (fn, time) => {  let info = {    arr: new Array(10 * 1024 * 1024).fill(1),    timer: null
 
    };  return function (...args) {
 
      info.timer && clearTimeout(info.timer);
 
      info.timer = setTimeout(() => {
 
        fn.apply(this, args);
 
      }, time);
 
    };
 
  };
 

(编辑:晋中站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章