博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
前端小秘密系列之闭包
阅读量:5982 次
发布时间:2019-06-20

本文共 7553 字,大约阅读时间需要 25 分钟。

本篇文章,我们来说说老(bei)生(xie)常(lan)谈(le)的,很多文章包括一些权威书籍中对于闭包的解释不尽相同,每个人的理解也都不一样。并且在其他语言中,也有对闭包的不同实现,让我们来看看 Javascript 中是如何实现闭包的以及有哪些特性。

直接进入主题,上一段简短的代码:

function outer(count) {    var temp = new Array(count)        function log() {        console.log(temp)    }        log()        function inner() {        console.log('done')    }        return inner}var o = {}for(var i = 0; i < 1000000; i++) {    o["f"+i] = outer(i)}复制代码

如果你不知道这段代码可能带来的问题,那么这篇文章就值得你读一读。

执行上下文 & 作用域链

我们先把上面的问题放一放,先让我们来看一看下面这段简单的代码:

function outer() {    var b = 2        function inner() {        console.log(a, b)    }        return inner}var a = 1var inner = outer()inner()复制代码

JS引擎 中,是通过执行上下文栈来管理和执行代码的。上述代码的伪执行过程如下(本节内容主要参考冴羽大大的):

0、程序开始

ECStack = []复制代码

1、创建全局上下文globalContext,并将其入栈

ECStack = [   globalContext ]复制代码

2、在执行之前初始化这个全局上下文

globalContext = {    VO: {        a: undefined,        inner: undefined,        outer: function outer() {...}    },    Scope: [globalContext]}复制代码

初始化作用域链属性 Scope[globalContext],此时代码还没有执行,由于变量提升的缘故, innera 变量为 undefined,需要注意的是,这个时候,outer 函数的作用域 [[scope]] 内部属性已确定(静态作用域):

outer.[[scope]] = [    globalContext.VO]复制代码

3、执行 globalContext 全局上下文

在执行过程中,不断改变 VO,执行到 a = 1 语句,将 VO 中的 a 置为 1,执行到 inner = outer() 语句,执行 outer 函数,进入 outer 函数的执行上下文。

4、创建outerContext执行上下文,将其入栈

ECStack = [    outerContext,    globalContext]复制代码

5、初始化 outerContext 执行上下文

outerContext = {    VO: {        b: undefined,        inner: function inner() {...}    },    Scope: [VO, globalContext.VO]}复制代码

初始化作用域链属性 Scope[VO].concat(outer.[[scope]])[VO, globalContext.VO]。 并在此时,确定 inner 函数的 [[scope]] 属性:

inner.[[scope]] = [    outerContext.VO,    globalContext.VO]复制代码

6、执行 outerContext 上下文

执行语句 b = 2,将 VO 中的 b 置为 2,最后返回 inner

7、outerContext 执行完毕,出栈,继续回到 globalContext 执行余下的代码

ECStack = [    globalContext]复制代码

继续执行 inner = outer() 语句的赋值操作,将 outer 函数的返回结果赋给 inner 变量。

执行 inner() 语句,进入 inner 函数的执行上下文。

8、创建 innerContext 执行上下文,将其入栈

ECStack = [    innerContext,    globalContext]复制代码

9、初始化 innerContext 执行上下文

innerContext = {    VO: {},    Scope: [VO, outerContext.VO, globalContext.VO]}复制代码

初始化作用域链属性 Scope[VO].concat(inner.[[scope]])[VO, outerContext.VO, globalContext.VO]

10、执行 innerContext 上下文

执行语句 console.log(a, b)VO 中没有变量 a,往上查找到 outerContext.VO,找到变量 aVO 中没有变量 b,依次往上查找到 globalContext.VO,找到变量 b。执行 console.log 函数,这里同样涉及到 变量console 的作用域链查找,console.log 函数的执行上下文切换,不再赘述。

11、globalContext 执行完毕,出栈,程序结束

ECStack = []复制代码

在第7步中,outerContext 执行完毕后,虽然其已出栈并在随后被垃圾回收机制回收,但是可以看到 innerContext.Scope 仍有对 outerContext.VO 的引用。当 outerContext被回收后,outerContext.VO 并不会被回收,如下图:

这就使得我们在执行 inner 函数时仍可以通过其作用域链访问到已执行完毕的 outer 函数中的变量,这就是闭包。

通过执行上下文和作用域链相关知识,我们引出了闭包的概念,让我们继续。

在第5步中,我们说到,初始化 outerContext 的过程中,同时确定了 inner 函数的作用域属性 [[scope]][outerContext.VO, globalContext.VO],这其实是不准确的。

我们稍微改动下加上两句代码:

function outer() {    var b = 2    var c = new Array(100000).join('*')    var d = 3        function inner() {        console.log(a, b)    }        return inner}var a = 1var inner = outer()inner()复制代码

聪明的你会发现,变量 cd 在inner中并不会用到,如果按照如上所述,将 inner 函数的 [[scope]] 属性置为 [outerContext.VO, globalContext.VO],那么变量 c (准确的说应该是变量 c 指向的那块内存,下同)只能一直等到 inner 函数执行完毕后才会被销毁,如果 inner 函数一直不执行的话,new Array(100000).join('*') 所占用的内存一直无法被释放。

那么,你可能会想,我们在确定 inner 函数 [[scope]] 属性的时候,只引用 inner 函数体内用到的变量不就好了吗?实际上,JS引擎 和你一样聪明,就是这么干的,在 Chrome 调试工具下:

可以看到,并没有对变量 c 的引用,我们可以认为 inner 函数 [[scope]] 属性为:

inner.[[scope]] = [    Closure(outerContext.VO),    globalContext.VO]复制代码

这里,我们用 Closure 这样一个函数来表示得到内部函数体中(包括内部函数中的内部函数,一直下去...)引用外部函数变量的集合,即闭包。

共享闭包

让我们继续前进的脚步,把上面的代码再稍微改动下:

function outer() {    var b = 2    var c = new Array(100000).join('*')    var d = 3        function log() {        console.log(c)    }        function inner() {        console.log(a, b)    }        log()        return inner}var a = 1var inner = outer()inner()复制代码

这里,我们只是加了一个 log 函数,并将变量 c 打印出来。对于 inner 函数来说,并没有什么改变,果真如此吗?我们看下 Chrome 调试工具下作用域和闭包相关信息。

outer函数执行之前:

outer函数执行完成:

咦,我们可以看到 inner 函数中的闭包中竟然包含了变量 c!但是 inner 函数中并没有用到 c啊,你可能隐隐发现了什么,是的,我们在 log 函数中引用了变量 c,这竟然会影响到 inner 函数的闭包。

在前文中,我们说到确定 inner 函数 [[scope]] 属性时,会通过 Closure 函数得到 inner 函数体内引用到的所有闭包变量集合,那有多个内部函数呢?

其实,JS引擎 会通过 Clousre 函数得到 outer 函数下所有内部函数体中用到的闭包变量集合 Closure(outerContext.VO) ,并且所有的内部函数的 [[scope]] 属性都引用这个共同的闭包,所以:

inner.[[scope]] = [    Closure(outerContext.VO),    globalContext.VO]log.[[scope]] = [    Closure(outerContext.VO),    globalContext.VO]Closure(outerContext.VO) = { b, c }复制代码

让我们来看看 log 函数的闭包信息,同样也有变量 b

这里,你可能会有疑问,变量 a 哪里去了,其实变量 aglobalContext 下。

读到这里,细心的你会发现,这和文章开头给出的代码几乎一毛一样啊,那究竟会带来什么问题呢,我想你应该知道了:内存泄露!

让我们回到文章开头的那段代码,返回的 inner 函数中,一直引用着 temp 变量,在 inner 函数不执行的情况下,temp 变量一直无法被垃圾回收。

我们再稍微改下代码:

function outer(count) {    var temp = new Array(count)        function log() {        console.log(temp)    }        log()        function inner() {        var message = 'done'                return function innermost() {            console.log(message)        }    }        return inner()}var o = {}for(var i = 0; i < 1000000; i++) {    o["f"+i] = outer(i)}复制代码

这里,我们在 inner 函数里面又包了一层,那最终返回的 innermost 还有对 temp 变量的引用吗?

按照前面关于执行上下文相关内容的逻辑分析下去,其实是有的。innermost[[scope]] 属性如下:

innermost.[[scope]] = [    Closure(innerContext.VO): { message },    Closure(outerContext.VO): { temp },    globalContext]复制代码

当然,你可能会说,只要 inner 函数执行完成后,这些内存就会被回收掉。OK,那我们再来看一个更经典的例子:

var theThing = null;var replaceThing = function () {  var originalThing = theThing;  var unused = function () {    if (originalThing)      console.log("hi");  };  theThing = {    longStr: new Array(1000000).join('*'),    someMethod: function () {      console.log(someMessage);    }  };};setInterval(replaceThing, 1000);复制代码

unused 函数引用了 originalThing ,由于共享闭包的特性,theThing.someMethod 函数的闭包中也包含了对 originalThing 的引用,而 originalThing 是上一个 theThing,也就是说下一个 theThing 引用者上一个 theThing,形成了一个链。并随着 setInterval 的执行,这个链越来越长,最终导致内存泄露,如下:

如果把间隔时间改小点,分分钟 out of memory

这个例子来源于,建议大家都点进去读一读(我记得之前有小伙伴翻译了这篇文章的,一时找不到了,有知道中文翻译链接的小伙伴在评论里贴一下哈)。

Real Local Variable vs Context Variable

Real Local Variable,直译过来就是真正的局部变量,在这里变量 d 就是 Real Local Variable,在C++层面,它可以直接分配在栈上,随着 inner 函数执行完毕的出栈操作而被立即回收掉,不需要后面垃圾回收机制的干预。

Context Variable,上下文环境变量或者称之为闭包变量,在这里变量 b 就是 Context Variable, 在C++层面,它一定分配在堆上,尽管这里它是一个基本类型。

那变量 c 呢,你可以认为它是一个 Real Local Variable,只是在栈上存的是指向这个 new Array() 的内存地址,而 new Array() 的实际内容是存在堆上的。

内存分布如下:

通过上面的分析,我们在很多文章中经常看到的 基本类型分布在栈上,引用类型分布在堆上 这句话明显是错误的,对于被闭包引用的变量,不管其是什么类型,肯定是分配在堆上的。

eval 与闭包

前文中已经提到,JS引擎 会分析所有内部函数体中引用了哪些外部函数的变量,但是对于 eval 的直接调用是无法分析的。因为无法预料到 eval 中可能会访问那些变量,所以会把外部函数中的所有变量都囊括进来。

function outer() {    var b = 2    var c = new Array(100000).join('*')    var d = 3        function inner() {        eval("console.log(1)")    }        return inner}var a = 1var inner = outer()inner()复制代码

JS引擎 内心OS是这样的:eval 这家伙什么事情都干的出来,你们(局部变量)统统不准走!

如果,你在层层嵌套的函数下面来一个 eval,那么 eval 所在函数的所有父级函数中的变量都无法被释放掉,想想就可怕...

那对于 eval 的间接调用呢?

function outer() {    var b = 2    var c = new Array(100000).join('*')    var d = 3        function inner() {        (0, eval)("console.log(a)")     // 输出1    }        return inner}var a = 1var inner = outer()inner()复制代码

这时 JS引擎 内心OS又是这样的:eval 是谁,不认识,你们(局部变量)都回家收衣服吧...

其实,对于 evalfunction 的组合还有各种姿势,比如:

function outer() {    var b = 2    var c = new Array(100000).join('*')    var d = 3        return eval("(function() { console.log(a) })")        // return (0,eval)("(function() { console.log(a) })")        // return (function(){ return eval("(function(){ console.log(a) })") })()        // ...        // 更多姿势留待各位自己去发掘和尝试,逃...}var a = 1var inner = outer()inner()复制代码

到这里就写完了,希望各位对闭包有一个新的认识和见解。

最后欢迎各路大佬们啪啪打脸...

转载于:https://juejin.im/post/5c723d90f265da2dc0065bdb

你可能感兴趣的文章
ARTS训练第三周
查看>>
vue中v-for循环如何将变量带入class的属性名中
查看>>
ceph学习笔记之七 数据平衡
查看>>
windows下的php的memcache扩展的安装及memcache最新下载地址
查看>>
YOLOv3: 训练自己的数据(绝对经典版本1)
查看>>
POJ 1150 The Last Non-zero Digit 《挑战程序设计竞赛》
查看>>
Could not find artifact com.sun:tools:jar:1.5.0 解决办法
查看>>
phpstorm xdebug remote配置
查看>>
引用与指针的区别
查看>>
pygtk笔记--2.1:布局容器,VBox、Hbox、Alignment
查看>>
dtree.js树的使用
查看>>
Springboot2.1.3 + redis 实现 cache序列化乱码问题
查看>>
线程什么时候需要同步,什么时候不需要同步?
查看>>
Struts2 自定义拦截器(方法拦截器)
查看>>
Linux服务器的那些性能参数指标
查看>>
BZOJ 2302: [HAOI2011]Problem c [DP 组合计数]
查看>>
c++ 11开始语言本身和标准库支持并发编程
查看>>
.NET Core 之 MSBuild 介绍
查看>>
iOS:即时通讯之<了解篇 SocKet>
查看>>
@EnableTransactionManagement注解理解
查看>>