当您可以提升嵌套函数时,如何概念化词法范围树?

How to conceptualize a lexical scope tree when you can have hoisted nested functions?

提问人:Lance 提问时间:1/3/2023 最后编辑:Lance 更新时间:2/3/2023 访问量:44

问:

我正在使用 TypeScript 开发编译器,并考虑了很多关于词法范围的问题。我特别想知道你如何处理你提升函数的情况,其中变量可以在一个点上未定义,然后在另一个点定义。例如:

function a() {
  let d = 10
  b()
  return b
  let g = 40
  function b() {
    c()
    let f = 30
    return c
    function c() {
      console.log(d, e, f, g)
    }
  }
  let e = 20
}
const x = a()() // returns b, returns c
x() // log the variables

在这里,函数内部的“词法范围”是什么?c

  • 在可能的代码评估期间,它是否是它可能/潜在地(在某个时候)访问的所有变量?(d, e, f, g)
  • 它最终只会在其运行时上下文中定义吗?(d, f)
  • 它不会被定义并引发错误吗?(d)
  • 是不是同时出现所有可能的父母组合(就像笛卡尔乘积一样,无论树中有多少父母)?
  • 等。

起初,有一段时间(很多年),我认为词汇范围是一个简单的树。您有更高的作用域和嵌套作用域,并且每个嵌套作用域都引入了新变量。很好,很干净,很容易。但是这个,这把它抛出了一个循环。现在就像,专注于 ,里面有评估步骤,在每个步骤中,可能存在不同的父范围。然后也可能有一套父作用域。所以它就像一朵向上开花的天然花朵/树(这在编程中很难想象)。有越来越多的可能的父母。我在想的是,词汇范围是父母向上的每一个组合的组合。但是,如何在编译器中使用此范围呢?cba

现在我很困惑。如果我不能为嵌套函数分辨出它是什么 1 范围定义,我应该在编译器中使用词法范围做什么?我假设每个嵌套函数都有 1 个作用域,但不是,它的父函数是庞大且组合的。只有当范围被绑定时,你才能真正知道它是什么(“绑定”,而不是“范围”)。也就是说,在代码评估的特定步骤中,作用域的值是多少。

那么,如何在编译器中使用这些信息呢?只有当我评估/模拟运行本身的代码时,我才能判断变量是否在范围内。基本上,我现在如何在编译器中使用这个词法范围?

我打算用它作为在嵌套词法范围块中定义哪些变量的真实来源。但现在我不能了,因为我需要在每一步都知道变量的值是什么,然后才能知道范围是什么。还是我错过了什么?

想尝试这样的事情:

type SiteContainerScopeType = {
  like: Site.ContainerScopeType
  parent?: SiteContainerScopeType
  children: Array<SiteContainerStepScopeType>
  declarations: Record<string, SitePropertyDeclarationType>
}

type SiteContainerStepScopeType = {
  like: Site.ContainerStepScope
  previous: SiteContainerStepScopeType
  context: SiteContainerScopeType
  declarations: Record<string, SitePropertyDeclarationType>
}
作用域 绑定 编译器构造 闭包 lexical-scope

评论


答:

1赞 coredump 2/3/2023 #1

如果我正确理解了 Understanding Hoisting in JavaScript 中描述的语义,则可以按如下方式重新组织代码,首先对声明进行排序,然后对变量初始化进行排序,最后对其余表达式进行排序。

function a() {
  // DECLARATIONS
  let d
  let g
  let e

  function b() {
    let f

    function c() {
      console.log(d, e, f, g)
    }

    f = 30
    c()
    return c
  }

  // INITIALIZATION

  e = 20
  d = 10
  g = 40

  // EXPRESSIONS

  b()
  return b
}

在执行此操作时,我们发现了一种更常见的结构,即在初始化和使用变量之前声明变量。