前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
本节内容对应协议内容 Reference Record
阅读这一章节之前,强烈建议先阅读前面的 语言类型和规范类型 章节。
定义
官方的解释: 引用记录类型用于解释诸如 delete、 typeof、赋值运算符、 super 关键字和其他语言特性等运算符的行为。例如,赋值的左边操作数应该生成一个引用记录。
赋值的左边操作数应该生成一个引用记录,这句话会在 标志符和作用域的章节得到详细的解释。
javascript
复制代码
var a = 10;
var obj = {};
obj.name = "object name";
console.log(a);
console.log(object.name);
思考一下, a 是什么, 10又是什么。
a 是标志符 Identifiers, 10 是 ECMAScript 语言类型 的 Numeric Types,即数值。
a = 10之后背后逻辑是 标志符 a 和 10 这个值,建立了某种绑定关系(后续会细说),之后可以标志符 a 访问到其值。
那为什么 console.log(a)会输出10呢, 这背后的执行逻辑
- 执行上下文通过 标志符 a 通过 ResolveBinding ( name [ , env ] ) 查找到 对应的 Reference Record 引用记录
- 然后通过 Reference Record 引用记录 取值 6.2.5.5 GetValue ( V )
- 最后输出值
这个Reference Record 是代码执行中的一种协议数据类型, 通常是需要对标志符进行操作时,生成的临时的引用关系的数据结构,接下来就来详细分析这个 Reference Record 。
引用记录字段
Reference Record 其字段名,值和含义如下:
| 字段名 | 值 | 含义 |
|---|---|---|
| [[Base]] | 语言类型的值,或者环境记录,或者unresolvable | 持有这个绑定关系的值或者环境记录。如果值是 unresolvable表示无法解析。 |
| [[ReferencedName]] | 字符串, Symbol, 或者 Private Name | 绑定的名称。如果[[Base]]是环境记录,其必为字符串。 |
| [[Strict]] | 布尔值 | 如果引用记录起源于严格模式代码,则为 true,否则为 false。 |
| [[ThisValue]] | 语言类型的值或者empty | 如果不为empty,引用记录表示使用 super 关键字表示的属性绑定; 它被称为 super 引用记录,其[[ Base ]]值永远不会是 环境记录。在这种情况下,[[ ThisValue ]]字段在创建引用记录时保存 this 值。 |
重点解释一下[[Base]]字段,因为其真的很重要, 真的很重要吗,真的很重要。
[[Base]]
值可能是语言类型的值,环境记录或者unresolvable。其是标志符取值操作的关键和核心。
javascript
复制代码
var a = 10;
console.log(a);
var obj = {};
obj.name = 'object name';
console.log(obj.name);
console.log(a) 执行时 ,通过标志符a查找, 会返回一个引用记录,其个字段的值如下:
| 字段 | 值 | 备注 |
|---|---|---|
| [[Base]] | 全局环境记录 | 返回的是环境记录。这个绑定关系是与环境记录的绑定。 |
| [[ReferencedName]] | a | |
| [[Strict]] | false | |
| [[ThisValue]] | empty |
console.log(obj.name) 执行时,通过标志符 name查找,会返回一个引用记录,其个字段的值如下:
| 字段 | 值 | 备注 |
|---|---|---|
| [[Base]] | obj | 返回的是语言类型的值。 这个绑定关系是 name和 obj的绑定,即所谓的 属性引用。 |
| [[ReferencedName]] | name | |
| [[Strict]] | false | |
| [[ThisValue]] | empty |
说完字段,再说一下引用记录的相关的抽象操作, 下面抽象方法的参数 V 都是 环境记录。
引用记录的抽象操作
IsPropertyReference ( V )
是否是属性引用。 如下的 obj.name就是属性引用。 而 aaaa则不是。
javascript
复制代码
var aaaa = 10;
var obj = {};
obj.name = 'object.name'
console.log(aaaa);
console.log(obj.name);
其逻辑就是检查字段[[Base]]的值,然后采用排查法。 就在上面已经清楚列出了[[Base]]值的情况,无非就三种, 排查前两种,那么就是属性引用。
[[Base]] 值的选项
- unresolvable
- 环境记录
- 语言类型的值
IsUnresolvableReference(V)
判断引用是否可达,即是否存在。 逻辑很简单,就是判断[[base]]字段的值是不是 unresolvable
未申明标志符aaaa 返回的引用记录就是不可达
javascriptjavascript...
复制代码
console.log(bbbb) // caught ReferenceError: bbbb is not defined
直接使用未申明的标志符一般都会抛出异常,也有例外
javascriptjavascript...
复制代码
typeof aaaa // "undefined"
至于为什么, 协议里做了明确的说明,引用不可达, 直接返回的是 "undefined"。
IsSuperReference ( V ) 【可以跳过】
就是判断 引用记录[[ThisValue]]的值是不是空值。
那你可能会问,这个值啥时候不是 empty呢?你在使用super[propertyName]或者 super.propertyName的时候,就会生成[[ThisValue]]不是 empty 的 的引用记录。
IsPrivateReference ( V ) 【可以跳过】
逻辑是通过判断引用记录的[[ReferencedName]]的值是不是 Private Name。用于class的私有字段。
这里出现了 Private Name,之前有提到过,是协议类型。 表示一个私有类元素(字段、方法或访问器)的键。每个私有名称有一个相关的不可变[[Description]]是一个字符串值。可以使用 PrivateFieldAdd 或 PrivateMethodOrAccessorAdd 在任何 ECMAScript 对象上安装私有名称,然后使用 PrivateGet 和 PrivateSet 读或写。
白话文就是 有一些内部不可变的字符串白名单。 添加,获取,设置都得通过内部的方法。
GetValue(V)
从引用记录中取值,这个是重点。 引用记录本身和取值之后返回的值的是大不同的。 分组运算符和属性方法调用都是拿着引用进行操作的。
可还记得,[[Base]] 的值可能是语言类型的值,也可能是环境记录。 所以值的来源就有两种方式,引用记录本身,还有环境记录,而环境记录还会嵌套。到这里,是不是想到了作用域的概念了, 细节会在对应的作用域链章节到来。
抽象方法逻辑如下:参数 V 就是引用记录
- 不可达,抛出 ReferenceError
- 如果是属性引用,直接从
[[Base]]上取值。 如果是私有引用(class),调用相关方法取值。
注意一下, 有ToObject的操作,然后用baseObj.[[Get]]去取值。 - 否则从环境记录里面去取值, 环境记录可以
outerENV关联外部的环境记录,形成链路。
再回顾[[Base]]提到的代码,
console.log(obj.name)中的obj.name是对象属性,直接去引用记录的[[Base]]去取值,也就是从对象上取属性值。console.log(a)中的a, 其不是对象属性,所以其[[Base]]是环境记录,从环境记录去取值。
javascript
复制代码
var a = 10;
var obj = {};
obj.name = 'object name';
console.log(obj.name);
console.log(a);
PutValue ( V, W )
给引用记录设置值(V为引用记录,W为值)。其实不能理解为引用记录设置值,实际上可能是给对象/全局对象添加属性,也可能是给环境记录新增绑定关系。
- 如果 V 不是引用记录,直接抛出错误
- 给未申明的标志符直接赋值,严格模式和非严格模式是有区别的。 非严格模式会在全局对象上新增属性,严格模式呢,抛出异常。
javascript
复制代码
function test(){
aaaa = 100;
console.log(globalThis.aaaa)
}
test(); // 100
console.log(aaa); // 100
javascript
复制代码
function test(){
"use strict"
aaaa = 100;
console.log(globalThis.aaaa)
}
test(); // caught ReferenceError: aaaa is not defined
2. 属性赋值。 有普通属性赋值,还有私有属性赋值。赋值在严格模式下还可能失败,比如属性描述符设置了不可写。
javascript
复制代码
class Person {
name = 'name';
#age = 18;
setAge(val){
this.#age = val // 私有属性赋值
}
}
var person = new Person();
person.name = 'new name'; // 普通属性赋值
person.setAge(16)
javascript
复制代码
function test(){
"use strict"
var obj = {
name: "name"
};
Object.defineProperty(obj, "name", {
writable : false
});
obj.name = 'new name';
};
test(); // caught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
- 如果上面逻辑都没走,就是走环境记录设置绑定关系。 环境记录有好几种,逻辑也有一些变化。具体后续会细说。
GetThisValue ( V )
属性引用记录时,获取this的值,这个和后面的函数调用 this 的值有关联, 函数调用的 this 的值,就是从 特定的环境记录借用的 [[ThisValue]]。
- 如果是 IsSuperReference(V),即class或者属性方法的super属性引用,直接返回
[[ThisValue]] - 不然就返回
[[Base]]
super 不仅仅是 class 可以使用,对象也是可以使用 super的
javascript
复制代码
var obj = {
name: "name",
toStr(){
console.log(super.toString())
}
};
obj.toStr() // [object Object]
到这里是不是想到了函数调用的this,这个还就真和函数调用有关系。至于具体的情况,函数章节细说。
InitializeReferencedBinding ( V, W ) 【可以跳过】
在引用记录的环境记录上初始化一个绑定。更多细节参见环境记录。 与初始化的还有一个概念,叫做实例化,后面会细说。
MakePrivateReference ( baseValue, privateIdentifier ) 【可以跳过】
创建一个私有引用记录。
可以从下面的协议的内容关联看到, fieldNameString 是一个 PrivateIdentifier, 再往下 其格式
# IdentifierName,这不就是class的私有属性的格式嘛。