本文的内容比较硬核,咱们一块儿来看下 JavaScript 全局变量的底层机制究竟是怎样的。文章会涉及脚本做用域、全局对象等概念。html
变量的词法做用域(简称做用域)是程序中能够访问它的区域。JavaScript 的做用域是静态的(在运行时不会改变),并能够嵌套——例如:前端
function func() { // (A)
const aVariable = 1;
if (true) { // (B)
const anotherVariable = 2;
}
}
复制代码
if
语句(B 注释行)引入的做用域嵌套在func()
(A注释行)的做用域内。web
包围做用域 S 的最内层的做用域称为 S 的外部做用域。在本例中, func
是if
的外部做用域。浏览器
在 JavaScript 语言规范中,做用域是经过词法环境实现的。它包括两个组成部分:bash
嵌套做用域的树形结构其实就是用互相连接的环境的树形结构表示的。微信
全局对象是一个特殊的对象,它的属性就是全局变量。(后面立刻讲到它是怎样跟环境树形结构对应上的)能够经过如下全局变量访问它:函数
globalThis
。 之因此叫这个名字,是由于它跟全局做用域里的this
值相同。window
是引用全局对象的经典方式。它能够在普通的浏览器代码中使用,但不能在Web Workers 和 Node.js 中使用。self
可用于浏览器(包括Web Workers),但不能在 Node.js 中使用。global
只在 Node.js 中可用。globalThis
并不直接指向全局对象在浏览器中,globalThis
并非直接指向全局对象的,而是间接指向的。例如,假设 web 页面里有个 iframe:ui
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 做用域
···
}
// (其余模块做用域)
}
复制代码
为了建立一个真正的全局变量,必须处于全局做用域——也就是在脚本的最顶层。
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>
复制代码
除了经过var
和函数声明建立的变量以外,全局对象还包含如下属性:
使用const
或let
保证全局变量声明不影响ECMAScript 和宿主平台的内置全局变量(或免受其影响)。 例如,浏览器有全局变量 .location
:
// 会改变当前文档的地址:
var location = 'https://example.com';
// 不会改变 window.location
let location = 'https://example.com';
复制代码
若是已经存在一个变量(例如本例中的location
),则带有初始化程序的var
声明的行为就相似于赋值。这就是咱们在此示例中遇到麻烦的缘由。
请注意,只有全局做用域才有这个问题。在模块中,不属于全局做用域(除非使用eval()
或相似的东西)。
为何 JavaScript 既有普通全局变量又有全局对象?
全局对象一般被认为是一个错误设计。所以,较新的特性实现,如 const
,let
,和class
则会建立普通全局变量(在 script 做用域内)。
值得庆幸的是,大多数用现代 JavaScript 编写的代码都位于ECMAScript 模块和 CommonJS 模块中。每一个模块都有本身的做用域,这就是为何控制全局变量的规则不多影响基于模块的代码。