原文地址:How do JavaScript’s global variables really work? 作者:Dr. Axel Rauschmayer
在这篇博客中,我们会探究JavaScript全局变量的运行机制。其中,有些有趣的现象将会起到关键作用,如作用域范围、全局对象等等。
一个变量能被程序所访问到的范围就是其词法作用域,简称作用域。Javascript的作用域是静态的,即不会在运行时改变,而且允许嵌套。例如:
function func() { // (A)
const foo = 1;
if (true) { // (B)
const bar = 2;
}
}
if 语句(B行)引入的作用域嵌套在函数func(A行)的作用域中。
某个作用域 S 的最近包含范围称为 S 的外部作用域。在上述示例中,if 的外部作用域就是函数 func。
在JavaScript语言规范中,作用域是通过词法环境实现的。其由两个部分组成:
因此,嵌套的上下文环境就是嵌套的作用域,并由外部引用相互链接。
属性为全局变量的对象称为全局对象,其有几个不同的名字:
全局对象包含所有内置的全局变量。
全局作用域就是最外层的作用域,即不在有外部作用域,其对应的环境就是全局环境。每个环境通过一系列的外部引用链最终和全局环境相关联,而全局环境的外部引用就是null。
全局环境结合了两个环境记录(可参考下图):
数据结构
接下来将说明如何将对象记录和声明记录组合在一起。
为了创建一个真正的全局变量,该变量必须在全局作用域范围内,即处于脚本执行环境的顶层。例如:
<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>
每个模块都有自己的环境,它存储所有顶层声明——包括导入的。模块环境的外部环境就是全局环境。
全局对象的存在通常被认为是一个错误,因此,新的语法规范中(如const、let和class)可以创建普通的全局变量(在脚本作用域中)。
幸运的是,现在通过JavaScript编写的代码大多数都位于ECMAScript模块和CommonJS模块中,而每个模块都有自己的作用域范围。这就是为什么全局变量的相关规范对于基于模块编写的代码没太大意义。