You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
何处:当访问变量 a 时,引擎无法在 innerFunc 作用域内找到它,但是由于作用域链的存在,引擎可以在 func 中找到变量 a;
如何:有两种查找方式,分别是 LHS 和 RHS。前者就是找到该变量所在的容器,从而进行赋值;后者就是读取到该变量本身的值。比如 var a = b,b 就是 RHS 查找,a 就是 LHS 查找。
在了解了作用域之后,先引出一个问题:
Question 1: 在 JavaScript 中,作用域以及作用域链如何实现呢?
词法环境(Lexical Environment)
A Lexical Environment is a specification type used to define the association of identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code.
Before it is evaluated, all ECMAScript code must be associated with a realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.
作用域
在开始主要内容之前,先简单写一写作用域。
作用域是一套用于确定在何处以及如何查找变量的规则。这是 YDKJS 中给出的关于作用域的定义。我们知道 JavaScript 采用的是词法作用域(静态作用域),简单讲,JavaScript 的作用域是根据代码书写位置确定的。
现在给出这样一段代码,在这段代码中,
innerFunc
的作用域嵌套在func
的作用域之中,形成了作用域链:回到上面的定义:
a
时,引擎无法在innerFunc
作用域内找到它,但是由于作用域链的存在,引擎可以在func
中找到变量a
;var a = b
,b
就是 RHS 查找,a
就是 LHS 查找。在了解了作用域之后,先引出一个问题:
Question 1: 在 JavaScript 中,作用域以及作用域链如何实现呢?
词法环境(Lexical Environment)
根据定义,词法环境是一种存在于规范中的类型,用来定义标识符(也就是变量名)与变量及函数(也就是变量值)之间的映射关系。它由两部分组成:
我们可以将词法环境简单表示如下:
通常,一个词法环境会与特定的代码相关联,比如说函数声明、块或者
try...catch
中的catch
子句。每当这些代码被执行时,都会重新创建一个新的词法环境。现在,我们可以回答上文的 Question 1:
在规范中,作用域通过词法环境来“实现”,多个词法环境通过 outer reference 建立引用,“实现”了作用域链。
环境记录(Environment Record)
环境记录主要有两类:
var
声明、let
声明、const
声明、函数声明、class
以及模块导入。另外,还有 3 种特殊的环境记录,分别是全局环境记录(全局词法环境持有)、模块环境记录(模块词法环境持有)以及函数环境记录(函数词法环境持有)。其中函数以及模块环境记录都是一种声明式环境记录。本文接下来的内容会从全局代码的角度出发介绍全局词法环境及其相关概念,函数及模块就不展开了。
我们可以通过一张图来简单了解一下这些环境记录的作用及关系:
全局词法环境(global environment)
全局词法环境可以理解成全局作用域。其他所有的词法环境(包括函数词法环境以及模块词法环境)都可以通过 outer reference 将自己与全局词法环境连接起来,但是,由于全局词法环境没有外部的词法环境,所以它的外部引用是 null。它的环境记录不仅内置了一些标识符绑定,并且会有一个与之关联的全局对象(我们称之为 GlobalObject)来持有一部分绑定。我们暂且可以这样描述全局词法环境:
介绍完全局词法环境,我们先引出以下这些问题:
Question 2: 全局词法环境的环境记录中,为什么有两处保存标识符绑定的地方?
Question 3: 全局词法环境中的 GlobalObject 是指代哪个对象,window 吗?
接下来我们来看全局词法环境持有的环境记录,它被称之为全局环境记录。
全局环境记录(Global Environment Record)
全局环境记录其实并不是一个单一的环境记录,实际上它是由一个声明式环境记录以及一个对象环境记录组成。那么,这两个环境记录分别有什么作用呢?
首先来看对象环境记录。在介绍全局词法环境的时候已经写到:
现在我们应该知道,这个 GlobalObject 其实就是该对象环境记录的绑定对象。全局环境记录正是通过这个 GlobalObject 绑定了内置的对象,属性等,比如
undefined
、Array
、Object
,另外它也绑定了全局代码中的函数声明、var
声明等,并且可以通过全局作用域中的this
访问到它。举个例子,有这样一段代码:我们可以得到对应的全局词法环境如下:
我们知道
one
和two
都可以在window
对象上被找到。其实我们现在就可以回答上文的 Question 3:在浏览器中,全局词法环境中的 GlobalObject 其实就是window
对象。既然是
window
对象,那我们还可以通过window.three = 3
的方式来修改它。在规范中,为了区分是声明的方式还是直接显式修改 GlobalObject,全局环境记录会通过 [[VarNames]] 字段加以区分,one
标识符以及two
标识符会被添加到 [[VarNames]] 中,但three
不会。简单了解下 [[VarNames]] 的作用,比较下面两段代码:
第一段代码会抛出 SyntaxError 错误,而第二段不会,正是因为 [[VarNames]] 的作用。
除了对象环境记录持有的绑定之外,全局代码中其他的声明,比如
const
、let
以及class
,则会记录在声明式环境记录中。我们修改上述代码:相应地,全局词法环境也发生了变化:
简单了解全局环境记录之后,我们可以对全局词法环境的图示进行改进。这个图示也解答了上文的 Question 2:全局词法环境通过声明式环境记录以及对象环境记录的分工合作来持有绑定。
那么,全局词法环境及其环境记录是如何被创建的呢?在规范中,有一个 NewGlobalEnvironment 方法,它从外部接受 GlobalObject 以及 thisValue 参数,可以通过以下步骤来模拟它的行为:
Realm
关于这个名词的翻译,没有找到合适的中文,所以就不翻译了。
根据规范的定义来看,所有的 JavaScript 代码都会有一个关联的 realm,概念上讲,realm 包括一些内置的对象,一个全局词法环境,以及与该词法环境关联的所有代码。我们主要关注它的 3 个要素:Intrinsics、GlobalObject 以及 GlobalEnv。
在这里我们又提到了 GlobalObject,在上文介绍 NewGlobalEnvironment 方法的时候,我们只是简单说明了 GlobalObject 由外部传入,并没有说明来自哪里。这个问题在这一节就有答案。
我们先来看 realm 的创建过程,在代码执行之前,浏览器就会负责创建 realm(2021年1月29日:调用函数 InitializeHostDefinedRealm)。根据我们关注的 3 个要素,我把一个 realm 的创建过程简化为了两个步骤:
比如(它看起来与Object.prototype
、Function.prototype
、Array
以及undefined
等window
很像)(2021年1月29日:Intrinsics 中的属性都是一些内置对象,我们没办法直接访问到,比如 %Array%,是与Array
有关的);undefined
。所以一个 realm 的结构看起来就像这样:
接下来,浏览器则会负责 GlobalObject 和 GlobalEnv 的创建。
Intrinsics 上的属性会被添加到 GlobalObject 上(这也就是为什么上文说 Intrinsics 看起来与)(2021年1月29日:GlobalObject 的属性并不是来自于 Intrinsics,而是重新创建的),并将window
很像,window
的一部分内置属性就来自 Intrinsicsthis
指向它。随后 GlobalObject 以及this
就会作为参数传递给上文提到的 NewGlobalEnvironment 方法,参与 GlobalEnv 的创建,GlobalObject 也就成为了全局词法环境的绑定对象。小结
介绍了 全局词法环境、全局环境记录以及 Realm 之后,用一张图梳理一下这 3 者的联系:
如果给出这样一段代码:
大家应该都知道变量提升这个名词,从变量提升的角度分析,大家也许会把
function
、var
提升到顶部。但是现在,我们暂时忘掉这个概念,也不去纠结const
以及let
到底会不会提升。我们从词法环境的角度出发:假定浏览器已经创建了 realm 以及全局词法环境。
var
声明,需要取到全局词法环境的 GlobalObject;f1n
属性?目前没有;f1n
绑定到 GlobalObject 上,并初始化为 undefined;f1n
这个标识符字符串加入到 [[VarNames]] 列表中;function
声明,尝试从 GlobalObject 本身中获取f1n
属性;f1n
属性已存在,直接将f1n
修改为函数;f1n
标识符字符串?已存在,不会重复添加。function
声明,尝试从 GlobalObject 本身中获取f2n
属性;f2n
不存在,直接将函数绑定到 GlobalObject;var
声明,判断 GlobalObject 本身是否存在f2n
?已存在,不做操作;const
声明,取到全局词法环境的 [[DeclarativeRecord]];immutable
作为不可变绑定添加到 [[DeclarativeRecord]],此时未初始化;let
声明,将mutable
作为可变绑定添加到 [[DeclarativeRecord]],此时未初始化。最后执行阶段,依次执行:
f1n = 'f1n'
f2n = 'f2n'
immutable = 'immutable'
mutable = 'mutable'
,如果没有值,则赋值为undefined
根据规范的定义,完整的步骤要复杂的多,这里做了一定的简化。
The text was updated successfully, but these errors were encountered: