深入理解 JavaScript
请支持本书:购买捐赠
(广告,请勿屏蔽。)

5 深入探讨全局变量



在本章中,我们将详细介绍 JavaScript 中全局变量的工作原理。其中涉及几个有趣的现象:脚本的作用域、所谓的*全局对象*等等。

5.1 作用域

变量的*词法作用域*(简称:*作用域*)是指程序中可以访问该变量的区域。JavaScript 的作用域是*静态的*(它们在运行时不会改变),并且可以嵌套——例如

function func() { // (A)
  const aVariable = 1;
  if (true) { // (B)
    const anotherVariable = 2;
  }
}

if 语句(B 行)引入的作用域嵌套在函数 func()(A 行)的作用域内。

作用域 S 的最内层外部作用域称为 S 的*外部作用域*。在示例中,funcif 的外部作用域。

5.2 词法环境

在 JavaScript 语言规范中,作用域是通过*词法环境*“实现”的。它们由两个组件组成

因此,嵌套作用域树由通过外部环境引用链接的环境树表示。

5.3 全局对象

全局对象是一个对象,其属性将成为全局变量。(我们很快就会研究它究竟是如何融入环境树的。)可以通过以下全局变量访问它

5.4 在浏览器中,globalThis 并不直接指向全局对象

在浏览器中,globalThis 并不直接指向全局对象,而是存在间接关系。例如,考虑网页上的 iframe

文件 parent.html

<iframe src="iframe.html?first"></iframe>
<script>
  const iframe = document.querySelector('iframe');
  const icw = iframe.contentWindow; // `globalThis` of iframe

  iframe.onload = () => {
    // Access properties of global object of iframe
    const firstGlobalThis = icw.globalThis;
    const firstArray = icw.Array;
    console.log(icw.iframeName); // 'first'

    iframe.onload = () => {
      const secondGlobalThis = icw.globalThis;
      const secondArray = icw.Array;

      // The global object is different
      console.log(icw.iframeName); // 'second'
      console.log(secondArray === firstArray); // false

      // But globalThis is still the same
      console.log(firstGlobalThis === secondGlobalThis); // true
    };
    iframe.src = 'iframe.html?second';
  };
</script>

文件 iframe.html

<script>
  globalThis.iframeName = location.search.slice(1);
</script>

浏览器如何确保 globalThis 在这种情况下不会改变?它们在内部区分了两个对象

在浏览器中,globalThis 引用 WindowProxy;在其他任何地方,它都直接引用全局对象。

5.5 全局环境

全局作用域是“最外层”的作用域——它没有外部作用域。它的环境是*全局环境*。每个环境都通过一系列由外部环境引用链接的环境与全局环境相连。全局环境的外部环境引用为 null

全局环境记录使用两个环境记录来管理其变量

稍后将解释何时使用这两个记录中的哪一个。

5.5.1 脚本作用域和模块作用域

在 JavaScript 中,我们只在脚本的顶层处于全局作用域。相反,每个模块都有自己的作用域,它是脚本作用域的子作用域。

如果我们忽略将变量绑定添加到全局环境的相对复杂的规则,那么全局作用域和模块作用域的工作方式就好像它们是嵌套的代码块一样

{ // Global scope (scope of *all* scripts)

  // (Global variables)

  { // Scope of module 1
    ···
  }
  { // Scope of module 2
    ···
  }
  // (More module scopes)
}

5.5.2 创建变量:声明式记录与对象记录

为了创建一个真正的全局变量,我们必须处于全局作用域中——这只有在脚本的顶层才有可能

<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(globalThis.one); // undefined
  console.log(globalThis.two); // 2
</script>

5.5.3 获取或设置变量

当我们获取或设置一个变量,并且两个环境记录都有该变量的绑定时,声明式记录优先

<script>
  let myGlobalVariable = 1; // declarative environment record
  globalThis.myGlobalVariable = 2; // object environment record

  console.log(myGlobalVariable); // 1 (declarative record wins)
  console.log(globalThis.myGlobalVariable); // 2
</script>

5.5.4 全局 ECMAScript 变量和全局宿主变量

除了通过 var 和函数声明创建的变量之外,全局对象还包含以下属性

使用 constlet 可以保证全局变量声明不会影响(或受其影响)ECMAScript 和宿主平台的内置全局变量。

例如,浏览器具有 全局变量 .location

// Changes the location of the current document:
var location = 'https://example.com';

// Shadows window.location, doesn’t change it:
let location = 'https://example.com';

如果变量已经存在(例如本例中的 location),则带有初始化表达式的 var 声明的行为类似于赋值。这就是为什么我们在本例中会遇到麻烦。

请注意,这只是全局作用域中的一个问题。在模块中,我们永远不会处于全局作用域中(除非我们使用 eval() 或类似方法)。

10 总结了我们在本节中学习的所有内容。

图 10:全局作用域的环境通过*全局环境记录*管理其绑定,而全局环境记录又基于两个环境记录:一个*对象环境记录*,其绑定存储在全局对象中;一个*声明式环境记录*,它使用内部存储来存储其绑定。因此,可以通过向全局对象添加属性或通过各种声明来创建全局变量。全局对象使用 ECMAScript 和宿主平台的内置全局变量进行初始化。每个 ECMAScript 模块都有自己的环境,其外部环境是全局环境。

5.6 结论:为什么 JavaScript 同时拥有普通全局变量和全局对象?

全局对象通常被认为是一个错误。因此,较新的结构(如 constlet 和类)会创建普通的全局变量(在脚本作用域中)。

值得庆幸的是,现代 JavaScript 中编写的大多数代码都位于 ECMAScript 模块和 CommonJS 模块 中。每个模块都有自己的作用域,这就是为什么管理全局变量的规则很少适用于基于模块的代码的原因。

5.7 本章的扩展阅读和参考资料

ECMAScript 规范中的环境和全局对象

globalThis:

浏览器中的全局对象