ECMAScript 引用记录

234 阅读8分钟

前言

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呢, 这背后的执行逻辑

这个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 就是引用记录

  1. 不可达,抛出 ReferenceError
  2. 如果是属性引用,直接从 [[Base]]上取值。 如果是私有引用(class),调用相关方法取值。
    注意一下, 有 ToObject的操作,然后用 baseObj.[[Get]]去取值。
  3. 否则从环境记录里面去取值, 环境记录可以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为值)。其实不能理解为引用记录设置值,实际上可能是给对象/全局对象添加属性,也可能是给环境记录新增绑定关系。

  1. 如果 V 不是引用记录,直接抛出错误
  2. 给未申明的标志符直接赋值,严格模式和非严格模式是有区别的。 非严格模式会在全局对象上新增属性,严格模式呢,抛出异常。
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>'
  
  1. 如果上面逻辑都没走,就是走环境记录设置绑定关系。 环境记录有好几种,逻辑也有一些变化。具体后续会细说。

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的私有属性的格式嘛。