New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

關於 JS 作用域裡面的解釋 #1

Open
aszx87410 opened this Issue Sep 25, 2018 · 2 comments

Comments

Projects
None yet
2 participants
@aszx87410
Copy link

aszx87410 commented Sep 25, 2018

您好,首先先感謝你的詳細解釋,我覺得 JS 作用域這篇的思路特別好,透過 JS 引擎在執行時候的觀點可以把一些知識點講解的很清楚,例如說作用域跟變量提升。

然而我這邊有一個小問題,文章裡面說到:每当 JS 引擎发现一个函数调用 (注意是调用,而不是声明),它就会创建一个新的函数执行环境。,我想問一下如果是這樣的話,該如何解釋閉包?

例如說以下代碼:

function giveMeClosure() {
  var count = 0
  function closure() {
    console.log(count++)
  }
  return closure
}

var func = giveMeClosure()
func()

在最後一行的 func() 之前都沒有調用裡面的 closure,可是這時候因為已經離開了 giveMeClosure,它的執行環境已經被銷毀,這樣就沒辦法解釋閉包如何存取到 giveMeClosure 執行環境的活動變量。

想請問這一個部分應該如何解釋,是不是其實在聲明函數的時候就已經有創建作用域鏈了?或是有其他機制可以解釋這個行為?

感謝

@nightn

This comment has been minimized.

Copy link
Owner

nightn commented Sep 25, 2018

@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 系列文章,我觉得讲得非常的精彩。

@aszx87410

This comment has been minimized.

Copy link

aszx87410 commented Sep 26, 2018

很感謝你這麼快速且用心的回覆,我大致上理解了!
這幾天找了很多跟 ECMAScript 相關的解釋文章,就屬你這篇跟解读ECMAScript[1]——执行环境、作用域及闭包講解的最好,而且思路挺類似的,都是直接從語言內部的角度來解釋執行時候的模式,只要能把這些東西弄懂,無論是 scope、closure 或是 hoisting 都不是什麼問題。

你推薦的那個系列我之前在研究 call by value 以及 call by reference 的問題時有看過,你沒提醒我都忘記有那個系列了哈哈,我會再找時間去看看那系列的。

再次感謝樓主的回覆。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment