Chrome中delete undefined.a与undefined.a报错信息不一致?不遵循规范?

如下图所示,chrome(版本 66.0.3359.139(正式版本))中为何 typeof运算符 作用于 undefined.a 报错信息不同? […
关注者
10
被浏览
3,025
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏

谢谢 @贺师俊 @紫云飞 的回答,题主又翻了一遍ES规范,加上两位的答案,将自己的观点整理了一下:

undefined.a;  // (1)
delete undefined.a;  // (2)
void undefined.a;  // (3)
typeof undefined.a   // (4)

由于(1)/(3)/(4)行逻辑一致,后续说明只用(2)和(4)进行比较。

先贴一下规范里面关于 delete 和 typeof 运算符的运行时语义:

delete operator
typeof operator

无论是delete运算符,还是typeof运算符,第一步操作都是先对其 操作数表达式 进行计算;如果这一步抛出异常,在delete运算符中会通过第二步ReturnIfAbrupt(ref)直接往上层传递异常,而typeof运算符则继续执行 ?GetValue(val),相当于执行ReturnIfAbrupt(GetValue(val)) (前导符号"?"的含义可以参考ReturnIfAbrupt Shorthands)。再看看GetValue(val)抽象操作(参考下图),它的第一步便是进行ReturnIfAbrupt(V)检查,所以异常(即abrupt completion)传入GetValue后又被原封不动的传递了回来(下面会说明为什么给"原封不动"画重点),因此typeof运算符实际上也是将 操作数表达式 计算过程中抛出的异常给直接往上层传递了。因此按照ECMAScript来走的话,上述四行代码抛出的异常应该是相同的,其异常描述信息也应该一致。

为什么V8(chrome)中不一致呢?

上述四行代码都会先计算 undefined.a 表达式,在RequireObjectCoercible这一步抛出异常,根据开头对规范的分析,delete运算符会直接往上传递该异常对象;而 undefined.a / void undefined.a / typeof undefined.a 会继续调用GetValue抽象操作,V8为了提供更友好的错误信息,可能在GetValue这一步进行了封装或者优化(而不是按规范中的原封不动返回异常对象),所以异常的描述信息与delete undefined.a不同。更加神奇的是GetValue操作同时还更新了出错的位置,下图中delete undefined.a没有执行GetValue抽象操作,其出错位置没有被正确更新(at <anonymous>:1:1),其他几个出错位置都不是第一列,更新到了undefined.a中a属性所处的列:

注意出错位置

src/messages.h中定义了如下的对应关系:

T(UndefinedOrNullToObject, "Cannot convert undefined or null to object")
T(NonObjectPropertyLoad, "Cannot read property '%' of %")

另外,V8源码中ToObject抽象操作抛出异常的描述信息为MessageTemplate::kUndefinedOrNullToObject,对应的是"Cannot convert undefined or null to object",这也正是delete undefined.a抛出的异常的描述信息,因为计算 操作数表达式 的时候RequireObjectCoercible会间接调用ToObject抽象操作:

而与GetValue相关的代码在我们说的情况下确实会抛出一个新的TypeError异常,其描述信息是MessageTemplate::kNonObjectPropertyLoad,对应"Cannot read property '%' of %",这正是typeof运算符和void运算符抛出的异常的描述信息。

总结

delete运算符右侧操作数表达式进行计算之后,如果返回异常直接往上传递该异常,不执行GetValue抽象操作以及delete运算符本身的逻辑;typeof/void等运算符在对右侧操作数表达式进行计算之后,无论是否发生异常都会继续执行GetValue抽象操作。而v8中GetValue抽象操作对应的代码会对右侧操作数表达式计算过程中抛出的异常进行优化,以返回更友好的出错信息;delete运算符因为没有执行GetValue抽象操作而保留了原来的异常描述信息。

其实Edge中异常描述信息也是存在差异的(刚开始没注意):

至于这是不是一个由于优化而产生的BUG,可能不同的人有不同的看法;个人认为既然都是在undefined.a的计算过程中抛出了异常,那就应该返回相同的错误描述信息,因为按照规范这与使用delete运算符还是typeof运算符没有一毛钱关系,即使有优化也要保证所有的情况都被优化。不过,有些实现可能认为delete运算符不涉及GetValue抽象操作,那么delete作为一个特例,干脆在其右侧操作数表达式计算的过程中不适用RequireObjectCoercible(这是有悖于ES规范的),计算后也不产生Reference值,此时案例中undefined.a的计算过程也就不会抛出任何异常,delete undefined.a是否抛出异常以及应该抛出什么异常就完全依赖于delete运算符本身的逻辑了。

【注意:undefined.a的正常计算不能产生base value component为undefined的Reference,必须直接抛出异常,否则就可以通过 undefined.a = 5 给全局对象添加属性了;而 a(未定义的情况下)会产生base value component为undefined的Reference,此时在非严格模式下可以通过 a = 5 给全局对象添加新的属性】


当然,这是一个"吃饱了撑的”的问题,该不该纠结于此那就仁者见仁智者见智了。只要理顺了关系,知道了原理,结果也就不重要了!