脚本范围的目的是什么?

What is the purpose of the script scope?

在 DevTools 控制台中检查函数的作用域时,我注意到一个 "script" 作用域。经过一些研究,它似乎是为 letconst 变量创建的。

没有 constlet 变量的脚本中函数的范围:

具有 let 变量的脚本中函数的作用域:

然而在控制台中打印了以下内容 1 - 脚本作用域中的变量仍然可以从其他脚本访问:

<script>let v = 1</script>
<script>console.log(v)</script>

我听说过 ES6 模块中的顶级变量无法从模块外部访问。这是示波器的用途还是有其他用途?

当您在顶层(即不在函数内部)使用 var 声明一个变量时,它会自动成为一个全局变量(因此在浏览器中您可以将其作为 属性 window)。它与使用 letconst 声明的变量不同——它们不会成为全局变量。您可以在另一个脚本标记中访问它们,但不能将它们作为 window.

的属性访问

看这个例子:

<script>
  var test1 = 42;
  let test2 = 43;
</script>
<script>
  console.log(test1); // 42
  console.log(window.test1); // 42
  console.log(test2); // 43
  console.log(window.test2); // undefined
</script>

JavaScript 没有“脚本作用域”。¹您所看到的正是 Google 的 V8 JavaScript 引擎所称的全局环境部分它包含在全局范围内使用 letconstclass 时创建的词法范围全局变量的新样式。它们仍然是全局变量,但它们不同于由 var 创建的旧式全局变量和全局范围内的函数声明(V8 在 Global 下的 [[Scopes]] 中显示)。 V8 调试器在这两个不同的地方列出了两种类型的全局变量。

如果你愿意,你可以在这里停止阅读,但如果你想要了解细节,请继续阅读。 :-)

那么为什么全球环境有两个全球部分?一句话:历史。

JavaScript 的原始形式的全局变量(global var-scoped bindings²)有多个问题。主要的两个是:

  • 它们不仅仅是全局可用的标识符,它们也是全局对象的属性(this 在全局范围内,也可以通过浏览器上的 window 全局或较新的 globalThis 规范定义的全局)。这意味着您可以在全局对象中查找您不知道其名称的内容(通过使用 for-inObject.keys 或类似的方法)。
  • 对同一标识符的重复声明不是错误。

除了全局范围内的那些问题之外,var 还存在没有块范围的问题;和块中的函数声明(也创建 var-scoped 绑定)未指定但允许作为扩展,导致它们在 JavaScript 实现中很大程度上不兼容的语义。

当需要添加一种声明具有更好语义的事物的新方法时(letconstclass;“词法范围绑定”),委员会认为moves JavaScript forward (ECMA TC39) 必须弄清楚这些新语义如何在全局范围内工作。他们的解决方案是将全局环境分为两部分 - 一个用于旧样式,另一个用于新样式 - 但仍然“逻辑上”将其视为单一环境。来自 the specification:

A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record.

“环境记录”是一个概念对象,它包含绑定²(变量等)和其他一些东西。将其与您在屏幕截图中看到的内容结合起来:

  • “对象环境记录”是将全局对象的属性用于 var 范围绑定的记录。这就是 V8 在 [[Scopes]].
  • 下调用的 Global
  • “声明性环境记录”是保存词法范围绑定的记录(直接,不在单独的对象中)。这就是 V8 在 [[Scopes]].
  • 下调用的 Script

在您的屏幕截图中,您有 let f,它创建了一个名为 "f" 的词法范围绑定,因此 V8 在 [[Scopes]].Script 下显示了它。如果你有 var f,V8 会在 [[Scopes]].Global 下显示。但同样,两者都是全局变量。


当他们说全球环境的两个部分“在逻辑上”是一个记录时,这是什么意思?基本上他们的意思是它 而不是 只是两个嵌套环境(尽管在很多方面它的行为就像它一样),只有 一个 全局范围(甚至尽管与之相关的环境有两个部分)。您可以看到的一种方法是您不能在全局范围内同时使用 varlet 声明某些内容,这是一个错误:

var a = 1;
let a = 2; // SyntaxError: Identifier 'a' has already been declared

如果它们只是 嵌套环境,您可以这样做 - 但那会是多么令人困惑!

但是虽然它们 不只是 嵌套,但它们 嵌套的。您可以通过在不使用 var 的情况下创建 var-scoped 全局来证明这一点(通过分配给全局对象上的 属性):

window.a = "var-scoped a";
let a = "lexically-scoped a";
console.log(a);         // "lexically-scoped a"
console.log(window.a);  // "var-scoped a"

let b = "lexically-scoped b";
window.b = "var-scoped b";
console.log(b);         // "lexically-scoped b"
console.log(window.b);  // "var-scoped b"

不言而喻,您不应该故意这样做,但它展示了双重环境的嵌套方面。


¹ 它确实有模块范围,这是不同的,但是像你这样的非模块脚本中的顶级代码是在全局范围内执行的。

² binding 是名称(如 a)和当前值的存储槽的组合。变量是绑定。常量、参数、函数声明创建的变量,以及各种内置的东西,如 this.