本篇文章,咱们来讲说老(bei)生(xie)常(lan)谈(le)的闭包,不少文章包括一些权威书籍中对于闭包的解释不尽相同,每一个人的理解也都不同。而且在其余语言中,也有对闭包的不一样实现,让咱们来看看
Javascript
中是如何实现闭包的以及有哪些特性。javascript
直接进入主题,上一段简短的代码:java
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)
}
复制代码
若是你不知道这段代码可能带来的问题,那么这篇文章就值得你读一读。git
咱们先把上面的问题放一放,先让咱们来看一看下面这段简单的代码:github
function outer() {
var b = 2
function inner() {
console.log(a, b)
}
return inner
}
var a = 1
var inner = outer()
inner()
复制代码
在 JS引擎
中,是经过执行上下文栈来管理和执行代码的。上述代码的伪执行过程以下(本节内容主要参考冴羽大大的系列文章):bash
0、程序开始闭包
ECStack = []
复制代码
一、建立全局上下文globalContext,并将其入栈函数
ECStack = [
globalContext
]
复制代码
二、在执行以前初始化这个全局上下文工具
globalContext = {
VO: {
a: undefined,
inner: undefined,
outer: function outer() {...}
},
Scope: [globalContext]
}
复制代码
初始化做用域链属性 Scope
为 [globalContext]
,此时代码尚未执行,因为变量提高的缘故, inner
和 a
变量为 undefined
,须要注意的是,这个时候,outer
函数的做用域 [[scope]]
内部属性已肯定(静态做用域):ui
outer.[[scope]] = [
globalContext.VO
]
复制代码
三、执行
globalContext
全局上下文spa
在执行过程当中,不断改变 VO
,执行到 a = 1
语句,将 VO
中的 a
置为 1
,执行到 inner = outer()
语句,执行 outer
函数,进入 outer
函数的执行上下文。
四、建立outerContext执行上下文,将其入栈
ECStack = [
outerContext,
globalContext
]
复制代码
五、初始化
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
]
复制代码
六、执行
outerContext
上下文
执行语句 b = 2
,将 VO
中的 b
置为 2
,最后返回 inner
。
七、
outerContext
执行完毕,出栈,继续回到globalContext
执行余下的代码
ECStack = [
globalContext
]
复制代码
继续执行 inner = outer()
语句的赋值操做,将 outer
函数的返回结果赋给 inner
变量。
执行 inner()
语句,进入 inner
函数的执行上下文。
八、建立
innerContext
执行上下文,将其入栈
ECStack = [
innerContext,
globalContext
]
复制代码
九、初始化
innerContext
执行上下文
innerContext = {
VO: {},
Scope: [VO, outerContext.VO, globalContext.VO]
}
复制代码
初始化做用域链属性 Scope
为 [VO].concat(inner.[[scope]])
即 [VO, outerContext.VO, globalContext.VO]
。
十、执行
innerContext
上下文
执行语句 console.log(a, b)
,VO
中没有变量 a
,往上查找到 outerContext.VO
,找到变量 a
,VO
中没有变量 b
,依次往上查找到 globalContext.VO
,找到变量 b
。执行 console.log
函数,这里一样涉及到 变量console
的做用域链查找,console.log
函数的执行上下文切换,再也不赘述。
十一、
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 = 1
var inner = outer()
inner()
复制代码
聪明的你会发现,变量 c
和 d
在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 = 1
var 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
哪里去了,其实变量 a
在 globalContext
下。
读到这里,细心的你会发现,这和文章开头给出的代码几乎一毛同样啊,那究竟会带来什么问题呢,我想你应该知道了:内存泄露!
让咱们回到文章开头的那段代码,返回的 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 = 1
var 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 = 1
var inner = outer()
inner()
复制代码
这时 JS引擎
心里OS又是这样的:eval
是谁,不认识,大家(局部变量)都回家收衣服吧...
其实,对于 eval
和 function
的组合还有各类姿式,好比:
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 = 1
var inner = outer()
inner()
复制代码
到这里就写完了,但愿各位对闭包有一个新的认识和看法。
最后欢迎各路大佬们啪啪打脸...