刨根问底:深刻研究 JavaScript 全局变量

本文的内容比较硬核,咱们一块儿来看下 JavaScript 全局变量的底层机制究竟是怎样的。文章会涉及脚本做用域、全局对象等概念。html

做用域

变量的词法做用域(简称做用域)是程序中能够访问它的区域。JavaScript 的做用域是静态的(在运行时不会改变),并能够嵌套——例如:前端

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

if 语句(B 注释行)引入的做用域嵌套在func()(A注释行)的做用域内。web

包围做用域 S 的最内层的做用域称为 S 的外部做用域。在本例中, funcif的外部做用域。浏览器

词法环境

在 JavaScript 语言规范中,做用域是经过词法环境实现的。它包括两个组成部分:bash

  • 一个将变量名映射到变量值的 环境记录。这是做用域内变量的实际存储空间。记录中的键值对称为 bindings
  • 外部环境 的引用——即外部做用域环境

嵌套做用域的树形结构其实就是用互相连接的环境的树形结构表示的。微信

全局对象

全局对象是一个特殊的对象,它的属性就是全局变量。(后面立刻讲到它是怎样跟环境树形结构对应上的)能够经过如下全局变量访问它:函数

  • 全平台可用的 globalThis。 之因此叫这个名字,是由于它跟全局做用域里的this值相同。
  • 其余几个针对特定平台的变量:
    • window是引用全局对象的经典方式。它能够在普通的浏览器代码中使用,但不能在Web Workers 和 Node.js 中使用。
    • self 可用于浏览器(包括Web Workers),但不能在 Node.js 中使用。
    • global 只在 Node.js 中可用。

浏览器中的globalThis 并不直接指向全局对象

在浏览器中,globalThis 并非直接指向全局对象的,而是间接指向的。例如,假设 web 页面里有个 iframe:ui

  • 每当 iframe 的src 发生变化时,它都会得到一个新的全局对象。
  • 可是 globalThis 的值老是保持不变。 能够用下面的例子来验证:

parent.html文件:this

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

  iframe.onload = () => {
    // 访问iframe全局对象的属性
    const firstGlobalThis = icw.globalThis;
    const firstArray = icw.Array;
    console.log(icw.iframeName); // 'first'

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

      // 不一样的全局对象
      console.log(icw.iframeName); // 'second'
      console.log(secondArray === firstArray); // false

      // 可是 globalThis 仍然是同样的
      console.log(firstGlobalThis === secondGlobalThis); // true
    };
    iframe.src = 'iframe.html?second';
  };
</script>
复制代码

iframe.html 文件:spa

<script>
  globalThis.iframeName = location.search.slice(1);
</script>
复制代码

浏览器是怎么保证 globalThis 不变的呢?其实,在内部是经过这两个对象来区分的:

  • Window 全局对象,随着地址改变而改变。
  • WindowProxy 代理对象,负责转发对当前Window的访问,这个对象不会改变。

globalThis 在浏览器中指向WindowProxy,在其余环境里中直接指向全局对象。

全局环境

全局做用域是“最外层”的做用域——它没有外层做用域。它的环境就是 全局环境。 每一个环境都连接到它的外层环境引用,造成一个链条,从而连接到全局环境。全局环境的外部环境引用值为null

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

  • 对象环境记录:跟普通环境记录的接口是同样的,只不过把 bindings  放在 JavaScript 对象里,也就是全局对象。

  • 普通(声明式)环境记录:具备本身的 bindings 存储。

这两个记录是怎么用的,后面会讲到。

脚本做用域和模块做用域

在 JavaScript 中,只有在脚本的顶层属于全局做用域。相反,每一个模块都有本身的做用域,它是脚本做用域的子做用域。

若是咱们忽略变量 bindings 添加到全局环境的复杂规则,全局做用域和模块做用域其实就像嵌套的代码块:

{ // 全局做用域 (*全部* script 的做用域)

  // (全局变量)

  { // module 1 做用域
    ···
  }
  { // module 2 做用域
    ···
  }
  // (其余模块做用域)
}
复制代码

建立变量:声明式记录和对象记录

为了建立一个真正的全局变量,必须处于全局做用域——也就是在脚本的最顶层。

  • 顶层 constlet和 class 在声明式环境记录中建立 bindings。
  • 顶层 var 和 函数声明在对象环境记录里建立 bindings。
<script>
  const one = 1;
  var two = 2;
</script>
<script>
  // 全部 script 共享同一个顶层做用域
  console.log(one); // 1
  console.log(two); // 2

  // 并不是全部声明都会建立全局对象的属性
  console.log(globalThis.one); // undefined
  console.log(globalThis.two); // 2
</script>
复制代码

读取和设置变量

当咱们获取或设置一个变量而且两个环境记录都具备该变量的 binding 时,则声明式记录优先级更高:

<script>
  let myGlobalVariable = 1; // 声明式环境记录
  globalThis.myGlobalVariable = 2; // 对象环境记录

  console.log(myGlobalVariable); // 1 (声明式记录优先)
  console.log(globalThis.myGlobalVariable); // 2
</script>
复制代码

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

除了经过var和函数声明建立的变量以外,全局对象还包含如下属性:

  • 全部内置 ECMAScript 全局变量
  • 全部内置宿主平台全局变量(浏览器、Node.js 等)

使用constlet保证全局变量声明不影响ECMAScript 和宿主平台的内置全局变量(或免受其影响)。 例如,浏览器有全局变量  .location:

// 会改变当前文档的地址:
var location = 'https://example.com';

// 不会改变 window.location
let location = 'https://example.com';
复制代码

若是已经存在一个变量(例如本例中的location),则带有初始化程序的var声明的行为就相似于赋值。这就是咱们在此示例中遇到麻烦的缘由。

请注意,只有全局做用域才有这个问题。在模块中,不属于全局做用域(除非使用eval()或相似的东西)。

总结

为何 JavaScript 既有普通全局变量又有全局对象?

全局对象一般被认为是一个错误设计。所以,较新的特性实现,如 constlet,和class 则会建立普通全局变量(在 script 做用域内)。

值得庆幸的是,大多数用现代 JavaScript 编写的代码都位于ECMAScript 模块和 CommonJS 模块中。每一个模块都有本身的做用域,这就是为何控制全局变量的规则不多影响基于模块的代码。

更多前端技术干货尽在微信公众号:1024译站

微信公众号:1024译站
相关文章
相关标签/搜索