原文标题:How do JavaScript’s global variables really work
地址:https://2ality.com/2019/07/global-scope.html
作用域
一个变量的词法作用域(简称作用域)是指:在程序的这个区域内可以访问到该变量。JavaScript的作用域是静态的(运行时不会改变),并且是可以嵌套的。例如:
function func() { // (A) const foo = 1; if (true) { // (B) const bar = 2; } }
由 If语句(B行) 创造的作用域是嵌套在函数func(A行)内的。
包含作用域S的外围作用域中最内部的作用域被称为S的外部作用域。在上面的例子中,func的作用域就是if的外部作用域。
Lexical environments(词法环境)
在JavaScript语言规范中,作用域是通过词法环境"实现"的,由下面两个部分组成:
- 一个environment record(环境记录,考虑成一个字典),变量名到变量值的映射。这是JavaScript保存变量的地方。一个在环境记录中的key-value入口被称作一次binding(绑定).
- 一个对outer environment(外部环境)的引用 —— 表示当前环境所处的作用域的外部作用域的环境
因此,一个嵌套的作用域树可以由一个嵌套的、由外部引用连接的environment树(环境树)表示。
全局对象
全局对象的属性都是全局变量。(我们一会儿将研究它到底是如何与环境树相适应的)。全局对象有不同的名字:
- 在所有环境中(提议中的future):globalThis
- 根据平台和语言接口的不同,全局对象有其他不同的名字:
- window:表示全局对象的最典型的方式。但是window仅在浏览器中生效。Node.js和Web Workers中都无法使用window。
- self:浏览器中都可以使用self表示全局对象,包括Web Workers。但是,Node.js不支持self。
- global:仅在Node.js中可用。
全局对象包含了所有的内置全局变量。
The global environment(全局环境)
全局作用域是"最外部"的作用域 —— 它没有外部作用域。它的环境是全局环境。所有的环境都通过一系列由外部引用连接的环境与全局环境相连。全局环境的外部引用为null。
全局环境结合了两个环境记录:
- object environment record (对象环境记录):像normal environment record(普通环境记录)一样工作,但是会保持它的bindings(绑定)和一个对象同步。这个对象就是全局对象。
- normal(declarative) environment record (普通声明环境记录)
下面的图表示了上述的数据结构。Script scope(脚本作用域)和 module environments (模块环境)将会在后面解释.
下面两个小结将会解释object record(对象记录)和declarative record (声明记录)是如何结合的。
创建变量
为了创建一个真正的全局变量,你必须处于全局作用域中 —— 这只是顶层脚本的情况:
- 顶层的const 、let 和 class 会在 声明记录中创建绑定.
- 顶层的 var 和 function 声明 会在对象记录中创建绑定.
<script> const one = 1; var two = 2; </script> <script> // All scripts share the same top-level scope: console.log(one); // 1 console.log(two); // 2 // Not all declarations create properties of the global object: console.log(window.one); // undefined console.log(window.two); // 2 </script>
另外,全局对象包含了所有的内置全局变量,并且通过对象记录将全部变量提供给全局环境。
获取、设置变量
当我们想要获取或设置一个在声明环境记录和对象环境记录都存在绑定的变量时,会优先操作声明环境记录。
<script> let foo = 1; // declarative environment record globalThis.foo = 2; // object environment record console.log(foo); // 1 (declarative record wins) console.log(globalThis.foo); // 2 </script>
Module environments(模块环境)
所有的模块都有自己的环境。它保存了所有顶级的声明 —— 包括imports。一个模块环境的外部环境就是全局的环境。
结论:为什么JavaScript即有正常的全局变量也有全局对象?
全局对象一般被认为是一个错误。因此,最新的const
, let
, 和 class 会创建正常的全局变量 (在script scope中时) 。
值得庆幸的是,大多数用现代JavaScript编写的代码都存在于ECMAScript模块和CommonJS模块中。每个模块都有自己的作用域,这就是为什么控制全局变量的规则对于基于模块的代码很少重要的原因。