详细聊聊浏览器是如何看闭包的


Posted in Javascript onNovember 11, 2021

前言

闭包,是javascript的一大理解难点,网上关于闭包的文章也很多,但是很少有能让人看了就彻底明白的文章。究其原因,我想是因为闭包涉及了一连串的知识点。只有把这一连串的知识点都理解透彻,实现一个概念的闭环,才可以真正理解它。今天打算换个角度来理解闭包,从内存分配与回收的角度阐述,希望能帮助你真正消化掉所看到的闭包知识,同时也希望本文是你看的最后一篇关于闭包的文章。

大家看本文中的配图时,请牢记箭头的指向。因为它是根对象window遍历内存垃圾所依赖的原则,能够从window开始,顺着箭头找到的都不是内存垃圾,不会被回收掉。只有那些找不到的对象才是内存垃圾,才会在适当的时机被gc回收。

闭包简介

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数被全局环境下的变量引用,就形成了闭包。

闭包实质上是函数作用域的副产物。

关于闭包我们需要特别重视的一点是函数内部定义的所有函数共享同一个闭包对象。什么意思呢?看如下代码:

var a
function b() {
  var c = new String('1')
  var d = new String('2')
  function e() {
    console.log(c)
  }
  function f() {
    console.log(d)
  }
  return f
}
a = b()

上面代码中f引用了变量d,同时f被外部变量a引用,所以形成闭包,导致变量d滞留在内存中。我们思考一下,那么变量c呢?好像我们并没有用到c,应该不会滞留在内存中吧。然后事实是c也会滞留在内存中。如上代码形成的闭包包含两个成员,c和d。这种现象成为函数内闭包共享。

为什么说需要特别重视这个特性呢?因为这个特性,如果我们不仔细的话,很容易写出导致内存泄漏的代码。
关于闭包的概念性的东西,我就讲这么多了,但是如果真正理解好闭包,还是需要搞明白几个知识点

  • 函数作用域链
  • 执行上下文
  • 变量对象、活动对象

这些内容大家可以谷歌百度之,大概理解一下。接下来我会讲如何从浏览器的视角来理解闭包,所以不做过多讲解。

如何判别内存垃圾

现代浏览器的垃圾回收过程比较复杂,详细过程大家可以自行google之。这里我只讲如何判定内存垃圾。大体上可以这么理解,从根对象开始寻找,只要能顺着引用找到的,都不能被回收。顺着引用找不到的对象被视为垃圾,在下一个垃圾回收节点被回收。寻找垃圾,可以理解为顺藤摸瓜的过程。

闭包的内存表示

从最简单的代码入手,我们看下全局变量定义。

var a = new String('小歌')

这样一段代码,在内存里表示如下

详细聊聊浏览器是如何看闭包的

在全局环境下,定义了一个变量a,并给a赋值了一个字符串,箭头表示引用。

我们再定义一个函数:

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
}

内存结构如下:

详细聊聊浏览器是如何看闭包的

一切都很好理解,如果你细心的话,你会发现函数对象teach里有一个叫[[scopes]]的属性,这是什么东东?函数创建完为什么会有这个属性。很高兴你能问到这一点,也是理解闭包很关键的一点。

请谨记: 函数一旦创建,javascript引擎会在函数对象上附加一个名叫作用域链的属性,这个属性指向一个数组对象,数组对象包含着函数的作用域以及父作用域,一直到全局作用域

所以上图可以简单理解为:teach函数是在全局环境下创建的,所以teach的作用域链只有一层,那就是全局作用域global

需要明确的是,浏览器下global指向window对象,nodejs环境global指向global对象

请再次谨记: 函数在执行的时候,会申请空间创建执行上下文,执行上下文会包含函数定义时的作用域链,其次包含函数内部定义的变量、参数等,当函数在当前作用域执行时,会首先查找当前作用域下的变量,如果找不到,就会向函数定义时的作用域链中查找,直到全局作用域,如果变量在全局作用域下也找不到,则会抛出错误。

我们都知道,函数执行的时候,会创建一个执行上下文,其实就是在申请一块栈结构的内存空间,函数中的局部变量都在这块空间中分配,函数执行完毕,局部变量在下一个垃圾回收节点被回收。OK,我们再次升级一下代码,看一下函数运行时内存的结构。

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
}
teach()

内存表示如下:

详细聊聊浏览器是如何看闭包的

很明显,我们可以看到,函数在执行过程中仅仅做了一个局部变量的赋值,并未与全局环境下的变量发生关系,所以我们从window对象沿着引用(图中的箭头)寻找的话,是找不到执行上下文中的变量b的。因此函数执行完后,变量b将被回收。

我们再次升级一下代码:

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
  var say = function() {
    console.log(b)
  }
  a =  say
}
teach()

内存表示如下:

详细聊聊浏览器是如何看闭包的

注:灰色表示的是无法从根对象跟踪到的对象。

函数执行顺序:

  1. 函数teach开始执行前,申请栈空间,上图蓝色方块。
  2. 创建上下文scope(类栈结构),并将teach函数定义时的[[scopes]]压入到scope中。
  3. 初始化变量b(变量提升),创建函数say,初始化say的scopes属性,首先将函数teach的scopes压入函数say的[[scopes]] 中。由于say引用了变量b,形成闭包closure。所以我们还要将closure对象压入函数say的[[scopes]]。
  4. 创建变量对象local,指向局部变量b和say,并将local压入步骤2的scope中。
  5. 函数开始执行
    1. 给变量b赋值字符串对象'小谷'。
    2. 将全局变量a指向函数say。

函数执行完毕,正常情况下变量b应该被释放了。但是我们发现,沿着window找下去,是能够找到b的,根据我们前面讲的判定内存垃圾的原理得知,b不是内存垃圾,所以b不能被释放,这就是为什么闭包会让函数内变量保存在内存中的原因。

再次升级代码,我们看下闭包共享的内存表示:

var a = new String('0')
function b() {
  var c = new String('1')
  var d = new String('2')
  function e() {
    console.log(c)
  }
  function f() {
    console.log(d)
  }
  return f
}
a = b()

详细聊聊浏览器是如何看闭包的

灰色表示的图形是内存垃圾,将会被垃圾回收器回收。

上图很容易得出,虽然函数f没有用到变量c,但是c被函数e引用,所以变量c存在于闭包closure中,从window对象开始寻找能够找到变量c,所以变量c也不能释放。

你也许会问了,这种特性是如何能导致内存泄漏的呢?好吧,思考如下一段代码,比较经典的meteor内存泄漏问题。

var t = null;
        var replaceThing = function() {
            var o = t
            var unused = function() {
                if (o)
                    console.log("hi")
            }
            t = {
                    longStr: new Array(1000000).join('*'),
                    someMethod: function() {
                      console.log(1)
                    }
                }
        }
        setInterval(replaceThing, 1000)

这段代码是有内存泄漏的,在浏览器中执行这段代码,你会发现内存不断上升,虽然gc释放了一些内存,但是仍然有一些内存无法释放,而且是梯度上升的。如下图

详细聊聊浏览器是如何看闭包的

这种曲线说明是有内存泄漏的,我们可以通过开发者工具去分析哪些对象没有被回收掉。事实上我可以告诉大家,没有释放掉的内存其实就是我们每次创建的大对象t。我们通过画图的方式来看下:

详细聊聊浏览器是如何看闭包的

上面这张图是假设replaceThing函数执行了三次,你会发现,每次我们给变量t赋予一个大对象的时候,由于闭包共享的缘故,之前的大对象仍然能够从window对象跟踪到,所以这些大对象都不能被回收掉。其实真正对我们有用的是最后一次为t赋予的大对象,那么之前的对象则造成了内存泄漏。

可以想象,假如我们没有意识到这一点,任由程序一直运行下去,浏览器很快就会崩溃。

解决这个问题的方式也很简单,每次执行完代码,将变量o置为null即可,大家可以试试看哈~

结语

到此这篇关于浏览器是如何看闭包的的文章就介绍到这了,更多相关浏览器如何看闭包内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
jquery $.ajax入门应用一
Nov 19 Javascript
比较全面的event对像在IE与FF中的区别 推荐
Sep 21 Javascript
JS拖动技术 关于setCapture使用
Dec 09 Javascript
Jquery 类网页微信二维码图块滚动效果具体实现
Oct 14 Javascript
jqplot通过ajax动态画折线图的方法及思路
Dec 08 Javascript
深入学习JavaScript对象
Oct 13 Javascript
初识angular框架后的所思所想
Feb 19 Javascript
一个Vue页面的内存泄露分析详解
Jun 25 Javascript
JS求1到任意数之间的所有质数的方法详解
May 20 Javascript
layui-table获得当前行的上/下一行数据的例子
Sep 24 Javascript
用VsCode编辑TypeScript的实现方法
May 07 Javascript
vue select 获取value和lable操作
Aug 28 Javascript
Vue3中的Refs和Ref详情
Nov 11 #Vue.js
react 路由Link配置详解
Nov 11 #Javascript
React Fragment介绍与使用详解
Nov 11 #Javascript
在js中修改html body的样式
Nov 11 #Javascript
用JS创建一个录屏功能
JavaScript数组 几个常用方法总结
Nov 11 #Javascript
JavaScript 事件捕获冒泡与捕获详情
You might like
简单实现限定phpmyadmin访问ip的方法
2013/03/05 PHP
PHP设计模式之命令模式的深入解析
2013/06/13 PHP
codeigniter中实现一次性加载多个view的方法
2015/03/20 PHP
PHP 8新特性简介
2020/08/18 PHP
js 巧妙去除数组中的重复项
2010/01/25 Javascript
js注意img图片的onerror事件的分析
2011/01/01 Javascript
extjs关于treePanel+chekBox全部选中以及清空选中问题探讨
2013/04/02 Javascript
关于JS中的闭包浅谈
2013/08/23 Javascript
详谈JavaScript 匿名函数及闭包
2014/11/14 Javascript
Jquery弹出层插件ThickBox的使用方法
2014/12/09 Javascript
Js删除数组中某一项或几项的几种方法(推荐)
2016/07/27 Javascript
详细分析Javascript中创建对象的四种方式
2016/08/17 Javascript
Bootstrap下拉菜单更改为悬停(hover)触发的方法
2017/05/24 Javascript
React实践之Tree组件的使用方法
2017/09/30 Javascript
vue计算属性时v-for处理数组时遇到的一个bug问题
2018/01/21 Javascript
浅谈layui 绑定form submit提交表单的注意事项
2019/10/25 Javascript
ES6 class类链式继承,实例化及react super(props)原理详解
2020/02/15 Javascript
Javascript地址引用代码实例解析
2020/02/25 Javascript
微信小程序自定义纯净模态框(弹出框)的实例代码
2020/03/09 Javascript
vue在响应头response中获取自定义headers操作
2020/07/24 Javascript
js实现鼠标点击飘爱心效果
2020/08/19 Javascript
[01:03:13]VG vs Pain 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
利用Python的Django框架中的ORM建立查询API
2015/04/20 Python
Python获取文件所在目录和文件名的方法
2017/01/12 Python
Python编程中flask的简介与简单使用
2018/12/28 Python
网易有道2017内推编程题 洗牌(python)
2019/06/19 Python
简单了解python中的与或非运算
2019/09/18 Python
pytorch下大型数据集(大型图片)的导入方式
2020/01/08 Python
Python3如何在服务器打印资产信息
2020/08/27 Python
Pytorch自定义Dataset和DataLoader去除不存在和空数据的操作
2021/03/03 Python
美国本地交易和折扣网站:LocalFlavor.com
2017/10/26 全球购物
2015年医德考评自我评价
2015/03/03 职场文书
紧急通知
2015/04/17 职场文书
2015年社区党务工作总结
2015/04/21 职场文书
MySQL 使用SQL语句修改表名的实现
2021/04/07 MySQL
Java常用函数式接口总结
2021/06/29 Java/Android