一文颠覆大众对闭包的认知

发表于 2年以前  | 总阅读数:387 次

网络上流传着许多对闭包的说法,这些说法为了方便理解曲解了闭包的真正原理,本文将会介绍这些原理,并且提供大量demo运行的结果来验证本文的正确性,注意:这可能会颠覆你对闭包的认知,请在家长的陪同下阅读!!!

闭包 & 内存泄漏

网络上对闭包的解释基本上都和 MDN 大同小异,“闭包就是访问了自由变量的函数”,其实这是为了大众方便理解而给出的错误结论(即使是这样似乎也有许多人无法理解闭包)

对于闭包产生的内存泄漏,网络中流传的大多数说法都是:“因为子函数执行时父函数的执行上下文已经退出执行上下文栈,但是由于子函数作用域链的引用导致父函数的 活动对象AO 无法被销毁”导致的。

其实上面的这两个广为流传的方法都是错误的,下面我将为你介绍真正的闭包和其内存泄漏的产生原理。

作用域链 [[Scopes]]

全局代码存储其变量的地方叫做变量对象(VO),函数存储其变量的叫活动对象(AO),VO 和 AO 都是在预编译时确定其内容,然后在代码运行时被修改值。

每一个函数都有一个 [[Scopes]] 属性,其存储的是这个函数运行时的作用域链,除了当前函数的 AO,作用域链的其他部分都会在其父函数预编译时添加到函数的 [[Scopes]] 属性上(因为父函数也需要预编译后才能确定自己的AO),所以 js 的作用域是词法作用域。

// 1: global.VO = {t}
let t = 111
function fun(){
    // 3: fun.AO = {a,b}
    let a = 1
    let b = 2
    function fun1() {
        // 5: fun1.AO = {c}
        let c = 3
    }
    // 4: fun1.[[Scopes]] = [...fun.[[Scopes]], fun.AO]
}
// 2: fun.[[Scopes]] = [global.VO]
fun()

上面代码在 fun() 被调用前,会立即预编译 fun 函数,这一步会得到 fun 的活动对象(AO),然后运行 fun 函数,在执行到 let a = 1 的时候,会将变量对象到 a 属性改成 1。后面也是一样

[[Scopes]] 就像一个数组一样,每一个函数的 [[Scopes]] 中都存在当前函数的 AO 和上级函数的 [[Scopes]]。在函数运行时会优先取距离当前函数 AO 近的变量值,这就是作用域的就近原则。

对于 作用域链AOVO 本文不详细介绍了,如果想详细了解可以 看这里

但是(重点来了)

上面介绍的 [[Scopes]] 可能就是大家熟知的,这在以前是对的。

但是最新的 V8 中已经发生了变化(Chrome 中已经可以看到这些变化),在为一个函数绑定词法作用域时,并不会粗暴的直接把父函数的 AO 放入其 [[Scopes]] 中,而是会分析这个函数中会使用父函数 AO 中的哪些变量,而这些可能会被使用到的变量会被存储在一个叫做 Closure 的对象中,每一个函数都有且只有一个 Closure 对象,最终这个 Closure将会代替父函数的 AO 出现在子函数的 [[Scopes]]

“网络上的说法是:父函数的 AO 直接会被放入子函数的 [[Scopes]] 中,也没有提到 Closure 对象,很明显这放在现在来看是不对的,当前后面我会给出例子证明。

闭包对象 Closure

在V8中每一个函数执行前都会进行预编译,预编译阶段都会执行3个重要的字节码

  1. CreateFunctionContext 创建函数执行上下文
  2. PushContext 上下文入栈
  3. CreateClosure 创建函数的闭包对象

也就是说,每一个函数执行前都会创建一个闭包,无论这个闭包是否被使用,那么闭包中的内容是什么?如何确定其内容?

Closure[[Scopes]] 一样会在函数预编译时被确定,区别是当前函数的 [[Scopes]] 是在其父函数预编译时确定, 而 Closure 是在当前函数预编译时确定(在当前函数执行上下文创建完成入栈后就开始创建闭包对象了)。

当 V8 预编一个函数时,如果遇到内部函数的定义不会选择跳过,而是会快速的扫描这个内部函数中使用到的本函数 AO 中的变量,然后将这些变量的引用加入 Closure 对象。再来为这个内部函数函数绑定 [[Scopes]] ,并且使用当前函数的 Closure 作为内部函数 [[Scopes]] 的一部分。

“注意:每一次遇到内部声明的函数/方法时都会这么做,无论其内部函数/方法的声明嵌套有多深,并且他们使用的都是同一个 Closure 对象。并且这个过程 是在预编译时进行的而不是在函数运行时。

// 1: global.VO = {t}
var t = 111
// 2: fun.[[Scopes]] = [global.VO]
function fun(){
    // 3: fun.AO = {a,b},并创建一个空的闭包对象fun.Closure = {}
    let a = 1,b = 2,c = 3
    // 4: 遇到函数,解析到函数会使用a,所以 fun.Closure={a:1} (实际没这么简单)
    // 5: fun1.[[Scopes]] = [global.VO, fun.Closure]
    function fun1() {
        debugger
        console.log(a)
    }
    fun1()
    let obj = {
        // 6: 遇到函数,解析到函数会使用b,所以 fun.Closure={a:1,b:2}
        // 7: method.[[Scopes]] = [global.VO, fun.Closure]
        method(){
            console.log(b)
        }
    }
}

// 执行到这里时,预编译 fun
fun()

“1、2发生在全局代码的预编译阶段,3、4、5、6、7发生在 fun 的预编译阶段。

fun1 执行时的作用域链是这样的:[fun1.AO, fun.Closure, global.VO]

我们可以看到 fun1 的作用域链中的确不存在 fun.AO ,而是存在 fun.Closure。并且 fun.Closure 中的内容是 ab 两个变量,并没有 c。这足以证明所有子函数使用的是同一个闭包对象。

细心的你会发现 Closuremethod 的定义执行前就已经包含 b 变量,这说明 Closure 在函数执行前早已确定好了,还有一点就是 Closure 中的变量存储的是对应变量的引用地址,如果这个变量值发生变化,那么 Closure 中对应的变量也会发生变化(后面会证明)

而且这里 fun1 并没有返回到外部调用形成网络上描述的闭包(网络上很多说法是需要返回一个函数才会形成闭包,很显然这也是不对的),而是直接在函数内部同步调用。

结论:每一个函数都会产生闭包,无论 闭包中是否存在内部函数 或者 内部函数中是否访问了当前函数变量 又或者 是否返回了内部函数,因为闭包在当前函数预编译阶段就已经创建了。

“是不是有点颠覆到你对闭包的认知了呢?别急,后面还有更多呢。

内存泄漏

说到闭包那么就不得不说内存泄漏,首先我们要搞清楚为什么会内存泄漏?

所谓闭包产生的内存泄漏就是因为闭包对象 Closure 无法被释放回收,那么什么情况下 Closure 才会被回收呢?

这当然是在没有任何地方引用 Closure 的时候,因为 Closure 会被所有的子函数的作用域链 [[Scopes]] 引用,所以想要 Closure 不被引用就需要所有子函数都被销毁,从而导致所有子函数的 [[Scopes]] 被销毁,然后 Closure 才会被销毁。

这与许多网络上的资料是不一样的,常见的说法是必须返回的函数中使用的自由变量才会产生闭包,也就是下面这样

function fun(){
    let arr = Array(10000000)
    return function(){
        console.log(arr);// 使用了 arr
    }
}
window.f = fun()

但是其实不然,即使返回的的函数没有访问自由变量,只要有任何一个函数将 arr 添加到闭包对象 Closure 中,arr 都不会正常被销毁,所以下面两段代码都会产生内存泄漏

function fun(){
    let arr = Array(10000000)
    function fun1(){// arr 加入 Closure
        console.log(arr)
    }
    return function fun2(){}
}
window.f = fun()// 长久持有fun2的引用

“因为 fun1arr 加入了 Closurefun2 又被 window.f 持有引用无法释放,因为 fun2 的作用域链包含 Closure,所以 Closure也无法释放,最终导致 arr 无法释放产生内存泄漏。

function fun(){
    let arr = Array(10000000)
    function fun1() {// arr 加入 Closure
        console.log(arr)
    }
    window.obj = {// 长久持有 window.obj.method 的引用
        method(){}
    }
}
fun()

“同理是因为 window.obj.method 作用域链持有 fun1Closure 引用导致 arr 无法释放。

那么我们将 arr = null 会不会让 arr 被释放呢?答案是会。这里有人可能会疑惑了:

Closure.arr = arrarr 加入到 Closure,然后将 arr = null,这为什么会让 Closure.arr 发生变化呢?

image.png

这说明将变量加入到 Closure 并不是简单的 Closure.arr = arr 的过程,这是一个引用传递,也就是说 Closure.arr 存储的是对变量 arr 的引用,当 arr 变化时 Closure.arr 也会发生变化。这对于 js 来说可能有点难实现,但是 c++ 借助指针的特性要实现这一点是轻而易举的。

上面我们简单的介绍了一下闭包产生内存泄漏的根本原因是因为 Closure 被其所有子函数的作用域链引用,只要有一个子函数没有销毁,Closure 就无法销毁,导致其中的变量也无法销毁,最终产生了内存泄漏。

“什么?看了这么多你告诉我你还不知道怎么看是否发生了内存泄漏?

打开Chrome浏览器的控制台的 Performance monitor,看到 JS heap size 变化曲线了吗?如果他不断上升并且你 点击 Memory 中这个垃圾回收的按钮后它依然没有下降到正常值,那么你的代码大概率是发生了内存泄漏,

现在我执行了一段上面的demo,可以看到内存大小是上升了一个量级

过了一段时间发现他并没有下降的趋势,即使我手动点击垃圾回收按钮,内存也没有回到最开始的正常值,很明显,这就是内存泄漏 如果你还没有捣鼓出这个界面来,建议先暂停一下然后去 谷歌 一下,因为后面的 demo 我不会贴出运行结果图,需要你自己在电脑上运行查看内存变化。

提升一下难度

下面是一个经典的内存泄漏的例子,在大多数与闭包内存泄漏的文章或者书籍中都能看到他的影子

let theThing = null;
let replaceThing = function () {
    let leak = theThing;
    function unused () { 
        if (leak){}
    };

    theThing = {  
        longStr: new Array(1000000),
        someMethod: function () {  

        }
    };
};

let index = 0;
while(index < 100){
    replaceThing()
    index++;
}

“为了防止各位看官轻易尝试导致电脑崩溃,我把原来例子中的 setInterval 换成了一个有限的循环

可能比较容易发现上面代码发生内存泄漏的原因是因为 someMethod ,因为 theThing 是全局变量导致 someMethod 无法释放最终导致 replaceThingClosure 无法释放。但是 replaceThingClosure 中存在什么呢?

let leak = theThing;
function unused () { // leak 加入 Closure
    if (leak){} 
};

是的,存在 leak,又因为 leak 指向的是 theThing 的值,虽然首次执行 replaceThingtheThingnull,但是第二次执行 replaceThingtheThing 就变为了一个存在大对象的对象了。

  1. 因为 Closure 无法释放导致其中的 leak 变量也无法释放,导致 theThing 无法释放
  2. theThing 会导致 someMethod 无法释放从而导致 Closure 无法释放

可能你已经看了几遍,最终开始看出了问题。没错,这是一个循环,theThing 导致 Closure 无法释放,Closure 又导致另一个 theThing 无法释放......

这段代码参数内存泄漏的原因可以是因为一环扣一环的引用引起的,我们吧第 ireplaceThing 执行时产生的 leak 叫做 leakitheThing 叫做 theThingiClosure 叫做 Closurei,如果这个函数执行3次,那么它的引用链路应该是这样的:

theThing3(全局作用域) -> someMethod3 -> Closure3 -> leak3 -> theThing2 -> someMethod2 -> Closure2 -> leak2 -> theThing1 -> someMethod1 -> Closure1 -> leak1 -> theThing0 -> null

image.png

可见 replaceThing 每执行一次这个链路中就会多一个 theThing,因为 theThing.longStr 上一个大对象导致内存飙升并且无法回收(引用的源头总是全局的 theThing )。

最粗暴的解决方法肯定是将全局 theThing 变为 null,这如同切断水流的源头一样。

但是在 replaceThing 的最后将 leak = null 也可以打破这个微妙的引用链路。因为这可以让 Closure 中的 leak 也变为 null 从而失去对 theThing 的引用,当在下一次执行 replaceThing 时会因为 theThing = xxx 导致原来的 theThing 失去最后的引用而回收掉,这也会让 theThing.someMethodClosure 可以被回收。

let theThing = null;
let replaceThing = function () {
    let leak = theThing;
    function unused () {
        if (leak){}
    };

    theThing = {  
        longStr: new Array(1000000),
        someMethod: function () {

        }
    };
    leak = null // 解决问题
};

let index = 0;
while(index < 100){
    replaceThing()
    index++;
}

总结

好了,现在我们来吧之前介绍的内容总结一下

  • 每一个函数在执行之前都会进行预编译,预编译时会创建一个空的闭包对象。
  • 每当这个函数预编译时遇到其内部的函数声明时,会快速的扫描内部函数使用了当前函数中的哪些变量,将可能使用到的变量加入到闭包对象中,最终这个闭包对象将作为这些内部函数作用域链中的一员。
  • 只有所有内部函数的作用域链都被释放才会释放当前函数的闭包对象,所谓的闭包内存泄漏也就是因为闭包对象无法释放产生的。
  • 我们还介绍的一个巧妙且经典的内存泄漏案例,并且通过一些demo的运行结果证明了上面这些结论的正确性。

不知道这些知识也没有颠覆你对闭包的认知呢?如果对文章有疑问欢迎评论,如果有收获感谢点赞。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ocB1ZklY-gPEOCajtaryhw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录