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


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 相关文章推荐
js函数般调用正则
Apr 08 Javascript
JS和jquery获取各种屏幕的宽度和高度的代码
Aug 02 Javascript
checkbox设置复选框的只读效果不让用户勾选
Aug 12 Javascript
对于Form表单reset方法的新认识
Mar 05 Javascript
JS实现屏蔽shift,Ctrl,alt等功能键的方法
Jun 01 Javascript
JavaScript中最容易混淆的作用域、提升、闭包知识详解(推荐)
Sep 05 Javascript
jQuery遍历节点方法汇总(推荐)
May 13 jQuery
Javascript实现倒计时时差效果
May 18 Javascript
jQuery封装placeholder效果实现方法,让低版本浏览器支持该效果
Jul 08 jQuery
Bootstrap框架建立树形菜单(Tree)的实例代码
Oct 30 Javascript
在Vue中使用axios请求拦截的实现方法
Oct 25 Javascript
vue+element 实现商城主题开发的示例代码
Mar 26 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
mysql From_unixtime及UNIX_TIMESTAMP及DATE_FORMAT日期函数
2010/03/21 PHP
php实现文件下载更能介绍
2012/11/23 PHP
thinkPHP删除前弹出确认框的简单实现方法
2016/05/16 PHP
php is_writable判断文件是否可写实例代码
2016/10/13 PHP
PHP echo()函数讲解
2019/02/15 PHP
双击滚屏-常用推荐
2006/11/29 Javascript
javascript之querySelector和querySelectorAll使用介绍
2011/12/20 Javascript
js中的屏蔽的使用示例
2013/07/30 Javascript
jquery阻止冒泡事件使用模拟事件
2013/09/06 Javascript
jquery显示隐藏input对象
2014/07/21 Javascript
nodejs URL模块操作URL相关方法介绍
2015/03/03 NodeJs
JS实现转动随机数抽奖特效代码
2020/04/16 Javascript
jQuery简单实现title提示效果示例
2016/08/01 Javascript
JS中的hasOwnProperty()、propertyIsEnumerable()和isPrototypeOf()
2016/08/11 Javascript
深入理解javascript中concat方法
2016/12/12 Javascript
jQuery日程管理插件fullcalendar使用详解
2017/01/07 Javascript
jQuery extend()详解及简单实例
2017/05/06 jQuery
javascript+html5+css3自定义弹出窗口效果
2017/10/26 Javascript
Webpack 4如何动态切割JS注入文件名详解
2019/07/09 Javascript
JS中的算法与数据结构之二叉查找树(Binary Sort Tree)实例详解
2019/08/16 Javascript
扫微信小程序码实现网站登陆实现解析
2019/08/20 Javascript
浅谈vue中使用编辑器vue-quill-editor踩过的坑
2020/08/03 Javascript
python文件比较示例分享
2014/01/10 Python
Python中的rjust()方法使用详解
2015/05/19 Python
Ubuntu安装Jupyter Notebook教程
2017/10/18 Python
浅析python中的迭代与迭代对象
2018/10/08 Python
纯CSS3制作的简洁蓝白风格的登录模板(非IE效果更好)
2013/08/11 HTML / CSS
River Island美国官网:英国高街时尚品牌
2018/09/04 全球购物
香港最大的洋酒零售连锁店:屈臣氏酒窖(Watson’s Wine)
2018/12/10 全球购物
Dr. Martens马汀博士法国官网:马丁靴鼻祖
2020/01/15 全球购物
为什么会有内存对齐
2016/10/10 面试题
linux面试题参考答案(1)
2016/01/22 面试题
优秀少先队辅导员事迹材料
2014/12/24 职场文书
党员带头倡议书
2015/04/29 职场文书
2015年高校保卫处工作总结
2015/07/23 职场文书
Nginx 负载均衡是什么以及该如何配置
2021/03/31 Servers