nightn/front-end-plan

關於 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 的問題時有看過,你沒提醒我都忘記有那個系列了哈哈,我會再找時間去看看那系列的。

再次感謝樓主的回覆。