關於 JS 作用域裡面的解釋
aszx87410 opened this issue · 2 comments
您好,首先先感謝你的詳細解釋,我覺得 JS 作用域這篇的思路特別好,透過 JS 引擎在執行時候的觀點可以把一些知識點講解的很清楚,例如說作用域跟變量提升。
然而我這邊有一個小問題,文章裡面說到:每当 JS 引擎发现一个函数调用 (注意是调用,而不是声明),它就会创建一个新的函数执行环境。
,我想問一下如果是這樣的話,該如何解釋閉包?
例如說以下代碼:
function giveMeClosure() {
var count = 0
function closure() {
console.log(count++)
}
return closure
}
var func = giveMeClosure()
func()
在最後一行的 func()
之前都沒有調用裡面的 closure
,可是這時候因為已經離開了 giveMeClosure
,它的執行環境已經被銷毀,這樣就沒辦法解釋閉包如何存取到 giveMeClosure 執行環境的活動變量。
想請問這一個部分應該如何解釋,是不是其實在聲明函數的時候就已經有創建作用域鏈了?或是有其他機制可以解釋這個行為?
感謝
@aszx87410
感谢你的关注和提问,你的问题非常很价值,我说一下我的个人理解。
首先,函数执行环境(Executation Context, EC)的确是在函数调用的时候创建的,而且每次调用时创建的执行上下文也是相互独立的。文章提到,执行上下文包括 3 个东西:变量对象(VO)或活动对象(AO),作用域链和 this。VO/AO 存储了当前函数内定义的所有变量、对象、函数以及形参等;this 与函数的调用方式相关;关于作用域链,很抱歉我没有完整详述,以下谈一谈作用域链。
作用域链虽然作为执行上下文的一部分,但却并不是等到执行上文创建时,它才开始创建的。一条完整作用域链可以认为分成两部分(无论是时间还是空间上),以下我以你的例子作为素材。
function giveMeClosure() { // line1
var count = 0 // line2
function closure() { // line3
console.log(count++) // line4
} // line5
return closure // line6
} // line7
// line8
var func = giveMeClosure() // line9
func() // line10
首先,每个函数都有一个 [[Scope]] 内部属性,它表示了这个函数被创建时所处的环境,在函数定义时就已经明确。[[Scope]] 的内容就是当前函数定义时,所处的执行上下文的作用域链。以上面程序为例,closure
函数在定义时就具有了一个 [[Scope]] 内部属性,该属性的内容就是 giveMeClosure
调用时所创建的执行上下文的作用域链,根据我那篇文章的分析,不难得出,giveMeClosure
的执行上下文长这样(以下简称 giveMeClosureEC
):
giveMeClosureEC = {
AO: {
arguments:{
length: 0
},
count: 0,
closure: Point to the function definition
},
scopeChain: [giveMeClosure.AO, globalEC.VO],
this: value of this
}
**closure
在定义时,它的 [[Scope]] 属性就已经确定为 giveMeClosureEC.scopeChain
**。随着 closure 被调用,其执行上下文对象 closureEC
被创建,closureEC
的作用域链,就等于 closure
函数的 [[Scope]] 属性加上 closureEC
的活动对象。即:
closureEC.scopeChain = closureEC.VO + closure.[[Scope]]
= closureEC.VO + giveMeClosureEC.scopeChain
= closureEC.VO + giveMeClosureEC.VO + giveMeClosure.[[Scope]]
= closureEC.VO + giveMeClosureEC.VO + globalEC.AO
= [closureEC.VO, giveMeClosureEC.VO, globalEC.AO]
其中,最核心的一句话:一个函数定义时就初始化其 [[Scope]] 属性为定义所处的作用域链,当它之后被调用时,会创建新的执行上下文,其中执行上文的作用域链就是:当前执行上下文的活动对象 + 函数的 [[Scope]] 属性。
从时间上,EC 中的作用域链是由函数定义和执行两个阶段构建的。
从空间上,EC 中的作用域链是由 funcEC.AO 和 func.[[Scope]] 两个部分构成的。
由于作用域链最核心的变化部分是 func.[[Scope]],而不是 funcEC.AO。所以可以认为:作用域链是在函数定义时就已经明确,这就是所谓的静态作用域(或词法作用域)。
所以,你最后所说的:
其實在聲明函數的時候就已經有創建作用域鏈
是可以这么理解的。这也解释了:无论闭包在哪里调用,它都能访问其定义时的自由变量(此处为 count
变量)。至于为什么 giveMeClosure
已经返回了,闭包还能取得 giveMeClosureEC.VO
,通过 [[Scope]] 也很解释得通了:因为 giveMeClosure
返回了 closure
,而 closure
的 [[Scope]] 属性还引用了 giveMeClosureEC.scopeChain
,尽管 closure
还未执行过,但这个 [[Scope]] 的引用在其定义时就已经建立,所以 JS 引擎不会去销毁 giveMeClosureEC.VO
的。
感谢你耐心的阅读,如有问题,欢迎继续提问。
另外,如果你想更深入地了解,我非常推荐你阅读 Dmitry Soshnikov ECMA-262-3 in detail 系列文章,我觉得讲得非常的精彩。
很感謝你這麼快速且用心的回覆,我大致上理解了!
這幾天找了很多跟 ECMAScript 相關的解釋文章,就屬你這篇跟解读ECMAScript[1]——执行环境、作用域及闭包講解的最好,而且思路挺類似的,都是直接從語言內部的角度來解釋執行時候的模式,只要能把這些東西弄懂,無論是 scope、closure 或是 hoisting 都不是什麼問題。
你推薦的那個系列我之前在研究 call by value 以及 call by reference 的問題時有看過,你沒提醒我都忘記有那個系列了哈哈,我會再找時間去看看那系列的。
再次感謝樓主的回覆。