你从互联网上学东西、掌握新的技能,或者互联网帮助你实现自我,那么互联网就是你的工具;如果你只是在互联网上玩乐,花了自己的时间和金钱,却只得到了精神的满足,那你是互联网的工具。在免费的江湖里,你就是产品 --《向上生长》
简明扼要
- 作用域是静态的
- 作用域通过词法环境实现的
- 词法环境 = 环境记录 + OuterEnv
- 变量的作用域链由词法环境中
OuterEnv
串联 - 在浏览器环境下,
globalThis
不直接指向全局对象 WindowProxy
是一个将所有访问转发到当前窗口的对象- 全局环境记录使用
对象环境记录
和声明环境记录
来管理变量 - 通过
const
,let
和class
创建的变量被绑定到声明环境记录中 - 通过
var
和函数声明的变量被绑定到对象环境记录中 - 声明环境变量中变量优先访问
- 使用ECMAScript和宿主环境的内置全局变量初始化全局对象
文章概要
- Scope(作用域)
- Lexical Environments (词法环境)
- 全局对象(global object)
- 浏览器环境下的
globalThis
- 全局环境(global envrionment)
- Script 作用域 和Module 作用域
- 生成变量: 声明环境记录 vs 对象环境记录
- 访问变量
- 全局ECMAScript变量和全局宿主变量
- 一图胜前言
1. Scope(作用域)
我们平时常说的变量的作用域(scope
),它全名应该叫词法作用域(lexical scope
)。它是程序中可以访问变量的区域,即作用域控制着变量和函数的可见性和生命周期。
我们在前期的文章中,描述了,V8执行JS代码核心流程 1. 先编译 2. 后执行。 在这个编译的过程就是静态的。所以我们可以这么说,作用域是不随代码的运行而改变的变量查找机制。
JS的作用域是静态的
同时,作用域还可以被嵌套。
function func() { // (A) const v1 = 1; if (true) { // (B) const v2 = 2; } } 复制代码
如上所示: 在B行的if
语句内嵌在A行的func()
函数作用域。
我们把内部作用域外面的作用域称为:outer
作用域。 例如:func
作用域就是if
作用域的 outer
作用域。
2. Lexical Environments (词法环境)
在ecma262(自备🪜)语言规范中定义:
作用域通过词法环境实现的
而词法环境由两个重要部分组成:
- 环境记录(environment record): 将变量名映射到变量值(类似于Map)。这是作用域变量的实际存储空间。记录中的名称-值条目称为绑定(
binding
)。 OuterEnv
内部属性:指向外部环境(outer environment)的引用
代码中嵌套的作用域树由指向外部环境的OuterEnv连接起来。即:
变量的作用域链由词法环境中
OuterEnv
串联
3. 全局对象
全局对象是其属性成为全局变量的对象。可以通过如下方式访问全局对象
globalThis
: 所有平台/宿主环境都可以访问,它与全局变量this
的值相等。
// 浏览器环境 `globalThis === this //true` // node REPL `globalThis === this //true` 复制代码
- 其他指向全局对象的变量只有在指定的平台才可以被访问
window
:它在浏览器主线程环境下生效,但是在Web Workers/Node
环境下失效self
: 在浏览器环境下生效(主/Web Worker),在Node
环境下失效global
: 只有在Node
环境下生效
4. 浏览器环境下的globalThis
在浏览器环境下,globalThis
不直接指向全局对象。
例如,现在有一个网页存在一个iframe
:
- 每当
iframe
中的src
的值发生变更,它会获得一个新的全局对象 - 无论
iframe
的src
的值如何变化,globalThis
的值一直不变
现在有两个html
1. parent 2. child
parent.html
<iframe src="child.html?first"></iframe> <script> const iframe = document.querySelector('iframe'); const icw = iframe.contentWindow; // iframe的`globalThis` iframe.onload = () => { // 通过globalThis访问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> 复制代码
child.html
<script> // 通过globalThis向全局对象新增一个binding(环境记录中的名称-值条目称为绑定) globalThis.iframeName = location.search.slice(1); </script> 复制代码
globalThis
通过两个内部属性保持值的不变:
Window
指向全局对象。每次变更location
(向window.location.href
赋值/通过改变iframe
的src
)它的值也会随之改变。WindowProxy
是一个将所有访问转发到当前窗口的对象。该对象永远不会改变
在浏览器环境下,globalThis
指向WindowProxy
;在其他环境下,globalThis
直接指向全局对象。
5. 全局环境
全局作用域是最外层的作用域:不存在包含它的作用域了。与之匹配的环境变量(environment)为全局环境(global environment)。每一个内部环境变量通过outerEnv
构建的作用域链最终与全局环境进行相连。全局环境的outerEnv
是null。
全局环境记录(注意:和全局环境有区别) 使用两类环境记录来管理变量
- 对象环境记录(object environment record ):将binding(环境记录中的名称-值条目称为绑定) 保存在对象中。在全局环境下,这个对象指向全局对象。
- 声明环境记录(declarative environment record): 拥有属于自己的存储空间来存放binding
5.1 Script 作用域 和Module 作用域
在JS中,只有在script
的顶层才属于全局作用域。相反的,每一个模块都有属于自己的作用域,而这个作用域作为全局作用域的子集。
通过一段伪代码来描述他们之间的关系:
{ // 全局作用域 // (全局变量) { // module 1 作用域 ··· } { // module 2 作用域 ··· } // (....) } 复制代码
5.2 生成变量: 声明环境记录 vs 对象环境记录
为了能够创建一个全局变量,我们需要在全局作用域下(script的顶层),进行变量的定义和赋值:
- 通过
const
,let
和class
创建的变量被绑定到声明环境记录中 - 通过
var
和函数声明的变量被绑定到对象环境记录中
<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> 复制代码
5.3 访问变量
当我们访问一个在声明环境记录和对象环境记录中都存在绑定的变量时
声明环境变量中变量优先访问。
<script> let gv = 1; // 存放到声明环境记录中 globalThis.gv = 2; // 存放到对象环境记录中 console.log(gv); // 1 声明环境变量中变量优先访问 console.log(globalThis.gv); // 2 </script> 复制代码
5.4 全局ECMAScript变量和全局宿主变量
除了通过var和函数声明创建的变量之外,全局对象还包含以下属性
- 所有ECMAScript内置的全局变量
- 宿主环境的所有内置全局变量(浏览器、Node.js等)
使用const
/let
定义的全局变量可以保证不受ECMAScript和宿主环境的内置全局变量影响。
例如,浏览器环境下,存在全局变量.location
// 改变当前页面的路径信息 var location = 'https://789.com'; // 将window.location截断了,并不会修改当前页面的页面信息 let location = 'https://789.com'; 复制代码
注意: 这种情况只有在全局环境下,才会发生
一图胜前言
全局作用域的环境通过一个全局环境记录来管理它的绑定,这个全局环境记录又基于两个环境记录:
- 对象环境记录,它的绑定存储在全局对象中
- 声明性环境记录使用内部存储来存储它的绑定
可以通过向全局对象添加属性或通过各种声明来创建全局变量。使用ECMAScript和宿主环境的内置全局变量初始化全局对象。每个ECMAScript模块都有自己的环境,其外部环境是全局环境。