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


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 相关文章推荐
showModelessDialog()使用详解
Sep 21 Javascript
JavaScript几种形式的树结构菜单
May 10 Javascript
asp.net 30分钟掌握无刷新 Repeater
Sep 16 Javascript
jquery remove方法应用详解
Nov 22 Javascript
AngularJS入门教程之链接与图片模板详解
Aug 19 Javascript
JS去除重复并统计数量的实现方法
Dec 15 Javascript
EasyUI为Numberbox添加blur事件的方法
Mar 05 Javascript
详解Vue路由开启keep-alive时的注意点
Jun 20 Javascript
详解组件库的webpack构建速度优化
Jun 18 Javascript
jQuery.validate.js表单验证插件的使用代码详解
Oct 22 jQuery
vue-cli2.0转3.0之项目搭建的详细步骤
Dec 11 Javascript
JS 设计模式之:单例模式定义与实现方法浅析
May 06 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
PHP在字符断点处截断文字的实现代码
2011/04/21 PHP
PHP类的声明与实例化及构造方法与析构方法详解
2016/01/26 PHP
php实现在站点里面添加邮件发送的功能
2020/04/28 PHP
jQuery实现跨域iframe接口方法调用
2015/03/14 Javascript
javascript基本语法
2016/05/31 Javascript
Vue.js实现无限加载与分页功能开发
2016/11/03 Javascript
浅谈原生JS中的延迟脚本和异步脚本
2017/07/12 Javascript
详解如何解决Vue和vue-template-compiler版本之间的问题
2018/09/17 Javascript
Vue用v-for给循环标签自身属性添加属性值的方法
2018/10/18 Javascript
使用 Vue cli 3.0 构建自定义组件库的方法
2019/04/30 Javascript
vue draggable resizable 实现可拖拽缩放的组件功能
2019/07/15 Javascript
JS求解两数之和算法详解
2020/04/28 Javascript
Linux下Python获取IP地址的代码
2014/11/30 Python
python爬取个性签名的方法
2018/06/17 Python
python TKinter获取文本框内容的方法
2018/10/11 Python
django 将model转换为字典的方法示例
2018/10/16 Python
python生成带有表格的图片实例
2019/02/03 Python
扩展Django admin的list_filter()可使用范围方法
2019/08/21 Python
Python urlencode和unquote函数使用实例解析
2020/03/31 Python
使用ITK-SNAP进行抠图操作并保存mask的实例
2020/07/01 Python
html5实现多文件的上传示例代码
2014/02/13 HTML / CSS
美国派对用品及装饰品网上商店:Shindigz
2016/07/30 全球购物
英国二手iPhone、音乐、电影和游戏商店:musicMagpie
2018/10/26 全球购物
新西兰网上购物,折扣店:BestDeals.co.nz
2019/03/20 全球购物
利用异或运算实现两个无符号数的加法运算
2013/12/20 面试题
服装设计专业毕业生推荐信
2013/11/09 职场文书
历史专业个人求职信范文
2013/12/07 职场文书
创业计划书撰写原则
2014/01/25 职场文书
社区庆八一活动方案
2014/02/02 职场文书
广告宣传策划方案
2014/05/21 职场文书
先进个人事迹材料(2016推荐版)
2016/03/01 职场文书
Nginx 负载均衡是什么以及该如何配置
2021/03/31 Servers
CSS3 天气图标动画效果
2021/04/06 HTML / CSS
Python办公自动化解决world文件批量转换
2021/09/15 Python
Python 详解通过Scrapy框架实现爬取CSDN全站热榜标题热词流程
2021/11/11 Python
一文搞懂Redis中String数据类型
2022/04/03 Redis