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

本文的内容比较硬核,我们一起来看下 JavaScript 全局变量的底层机制究竟是怎么的。文章会涉及脚本作用域、全局对象等概念。
作用域
变量的词法作用域(简称作用域)是程序中可以访问它的区域。JavaScript 的作用域是静态的(在运行时不会改变),并可以嵌套——例如:
function func() { // (A) const aVariable = 1; if (true) { // (B) const anotherVariable = 2; }}if 语句(B 注释行)引入的作用域嵌套在func()(A注释行)的作用域内。
包围作用域 S 的最内层的作用域称为 S 的外部作用域。在本例中, func是if的外部作用域。
词法环境
在 JavaScript 语言规范中,作用域是通过词法环境实现的。它包括两个组成部分:
- 一个将变量名映射到变量值的 环境记录。这是作用域内变量的实际存储空间。记录中的键值对称为 bindings。
- 对 外部环境 的引用——即外部作用域环境
嵌套作用域的树形结构其实就是用互相链接的环境的树形结构表示的。
全局对象
全局对象是一个特殊的对象,它的属性就是全局变量。(后面马上讲到它是怎么跟环境树形结构对应上的)可以通过以下全局变量访问它:
- 全平台可用的
globalThis。 之所以叫这个名字,是由于它跟全局作用域里的this值相同。 - 其余几个针对特定平台的变量:
window是引用全局对象的经典方式。它可以在普通的浏览器代码中使用,但不能在Web Workers 和 Node.js 中使用。self可用于浏览器(包括Web Workers),但不能在 Node.js 中使用。global只在 Node.js 中可用。
浏览器中的globalThis 并不直接指向全局对象
在浏览器中,globalThis 并不是直接指向全局对象的,而是间接指向的。例如,假设 web 页面里有个 iframe:
- 每当 iframe 的
src发生变化时,它都会取得一个新的全局对象。 - 但是
globalThis的值总是保持不变。 可以用下面的例子来验证:
parent.html文件:
<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 文件:
<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 作用域 ··· } // (其余模块作用域)}创立变量:公告式记录和对象记录
为了创立一个真正的全局变量,必需处于全局作用域——也就是在脚本的最顶层。
- 顶层
const,let和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 等)
使用const或者let保证全局变量公告不影响ECMAScript 和宿主平台的内置全局变量(或者免受其影响)。
例如,浏览器有全局变量 .location:
// 会改变当前文档的地址:var location = 'https://example.com';// 不会改变 window.locationlet location = 'https://example.com';假如已经存在一个变量(例如本例中的location),则带有初始化程序的var公告的行为就相似于赋值。这就是我们在此示例中遇到麻烦的起因。
请注意,只有全局作用域才有这个问题。在模块中,不属于全局作用域(除非使用eval()或者相似的东西)。
总结
为什么 JavaScript 既有普通全局变量又有全局对象?
全局对象通常被认为是一个错误设计。因而,较新的特性实现,如 const,let,和class 则会创立普通全局变量(在 script 作用域内)。
值得庆幸的是,大多数用现代 JavaScript 编写的代码都位于ECMAScript 模块和 CommonJS 模块中。每个模块都有自己的作用域,这就是为什么控制全局变量的规则很少影响基于模块的代码。
更多前台技术干货尽在微信公众号:1024译站
微信公众号:1024译站
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 刨根问底:深入研究 JavaScript 全局变量