理解 ECMAScript 规范-part1

原文 : https://v8.dev/blog/understanding-ecmascript-part-1

即使你了解JavaScript,阅读其语言规范,ECMAScript语言规范或简称为ECMAScript规范,也可能会感到相当艰巨。至少在我第一次阅读它时,就有这种感觉。

让我们从一个具体的例子开始,透过规范来理解它。以下的代码展示了Object.prototype.hasOwnProperty的使用:

const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false

在这个例子中,o 没有一个叫做hasOwnProperty的属性,所以我们沿着原型链向上寻找它。我们在o的原型,也就是Object.prototype中找到了它。

为了描述Object.prototype.hasOwnProperty是如何工作的,规范使用了类似于伪代码的描述:

Object.prototype.hasOwnProperty(V)

hasOwnProperty方法被调用并带有参数V时,将执行以下步骤:

  1. Let P be ? ToPropertyKey(V).
  2. Let O be ? ToObject(this value).
  3. Return ? HasOwnProperty(O, P).

还有。。。

抽象操作HasOwnProperty用于确定一个对象是否具有指定的属性键的自身属性。它将返回一个布尔值。此操作是带有参数OP的调用,其中O是对象,P是属性键。这个抽象操作执行以下步骤:

  1. Assert: Type(O) is Object.
  2. Assert: IsPropertyKey(P) is true.
  3. Let desc be ? O.[[GetOwnProperty]](P).
  4. If desc is undefined, return false.
  5. Return true.

但是,什么是“抽象操作”? [[ ]]内部的东西是什么?为什么在函数前面有一个?符号?断言意味着什么呢?

让我们去寻找答案!

语言类型和规范类型

让我们从一些看起来熟悉的东西开始。规范使用了像undefined, true和false这样我们已经从JavaScript中了解的值。它们都是语言值,也是规范定义的语言类型的值。

规范也在内部使用语言值,例如,一个内部数据类型可能包含一个可能的值是true和false的字段。相比之下,JavaScript引擎通常不在内部使用语言值。例如,如果JavaScript引擎是用C++编写的,那么它通常会使用C++的true和false(而不是它对JavaScript的true和false的内部表示)。

除了语言类型,规范还使用规范类型,这些类型仅出现在规范中,而不是在JavaScript语言中。JavaScript引擎不需要(而且可以自由地)实现它们。在这篇博客文章中,我们将了解规范类型Record(及其子类型Completion Record)。

抽象操作

抽象操作是在ECMAScript规范中定义的函数;它们的定义目的是为了简洁地编写规范。JavaScript引擎不必在引擎内部将它们实现为单独的函数。它们不能直接从JavaScript调用。

内部插槽和内部方法

内部插槽和内部方法使用 [[ ]] 包围。

内部插槽是一个JavaScript对象或规范类型的数据成员。它们用于存储对象的状态。内部方法是JavaScript对象的成员函数。

例如,每个JavaScript对象都有一个内部揽槽[[Prototype]]和一个内部方法[[GetOwnProperty]]。

内部插槽和方法不可从JavaScript访问。例如,你不能访问o.[[Prototype]]或调用o.[[GetOwnProperty]]。JavaScript引擎可以实现它们以供自己内部使用,但不一定要这样做。

(译者:这里还记得抽象操作和内部方法的区别吗?)

有时,内部方法会委派给同名的抽象操作,例如在普通对象的[[GetOwnProperty]]中:

[ [ GetOwnProperty ] ] (P)
当用属性键P调用O的[[GetOwnProperty]]内部方法时,将执行以下步骤:
返回 ! OrdinaryGetOwnProperty(O, P)。 

(我们将在下一章中找出感叹号的含义。)

OrdinaryGetOwnProperty不是一个内部方法,因为它没有与任何对象相关联;相反,它操作的对象被作为一个参数传递。

OrdinaryGetOwnProperty被称为“ordinary”,因为它操作的是普通对象。ECMAScript对象可以是普通类型或者奇特类型。普通对象必须对一组被称为基本内部方法的方法具有默认行为。如果一个对象偏离了默认行为,那么它就是奇特的。

最知名的奇特对象是数组,因为它的length属性的行为方式不是默认的:设置length属性可以从数组中移除元素。

基本内部方法在这里.

完成记录

那么问号和感叹号是什么呢?为了理解它们,我们需要研究一下完成记录!

完成记录是一个规范类型(仅为规范目的定义)。JavaScript引擎不需要有一个相应的内部数据类型。

完成记录是一个“记录”——一种具有固定的命名字段集的数据类型。完成记录有三个字段:

名字 描述
[[Type]] normalbreakcontinuereturnthrow。除了normal以外的所有类型都是突然完成
[[Value]] 完成发生时产生的值,例如,函数的返回值或异常(如果有的话)。
[[Target]] 用于定向控制转移(与本文无关)。

每个抽象操作都隐式返回一个完成记录。即使看起来抽象操作会返回一个简单类型,例如Boolean,它也会被隐式地包装成一个带有normal类型的完成记录(参见隐式完成值)。

注意1:在这方面,规范并不完全一致;有一些辅助函数返回裸值,其返回值被直接使用,而不是从完成记录中提取值。这通常可以从上下文中清楚地看到。

注意2:规范编辑者 正在研究如何更明确地处理完成记录。

如果一个算法抛出一个异常,这意味着返回一个带有Type throw和Value是异常对象的完成记录。我们现在暂时忽略break、continue和return类型。

ReturnIfAbrupt(argument)意味着执行以下步骤:

  1. 如果argument是突然的,那么返回argument
  2. 将argument设置为argument.[[Value]]。

也就是说,我们会检查一个完成记录;如果它是一个突然完成的记录,我们会立即返回。否则,我们会从完成记录中提取值。

ReturnIfAbrupt可能看起来像函数调用,但事实并非如此。它会导致出现ReturnIfAbrupt()的函数返回,并非ReturnIfAbrupt函数本身返回。它的行为更像C-like语言中的宏。

ReturnIfAbrupt可以这样使用:

  1. 让 obj 成为 Foo() 。(obj是一个完成记录。)
  2. ReturnIfAbrupt(obj)。
  3. Bar(obj)。 (如果我们还在这里,那么obj就是从完成记录中提取出的值。)

现在问号起作用了:? Foo()相当于ReturnIfAbrupt(Foo())。使用简写是非常实用的:我们不需要每次都明确写出错误处理代码。

类似地,Let val be ! Foo()相当于:

  1. 让 val 成为 Foo()。
  2. 断言:val 不是一个突然的完成。
  3. 将val设置为val.[[Value]]。

利用这些知识,我们可以像这样重写Object.prototype.hasOwnProperty:

  Object.prototype.hasOwnProperty(V)

1.Let P be ToPropertyKey(V).
2.If P is an abrupt completion, return P
3.Set P to P.[[Value]]
4.Let O be ToObject(this value).
5.If O is an abrupt completion, return O
6.Set O to O.[[Value]]
7.Let temp be HasOwnProperty(O, P).
8.If temp is an abrupt completion, return temp
9.Let temp be temp.[[Value]]
10.Return NormalCompletion(temp)

…我们也可以像这样重写 HasOwnProperty :

  HasOwnProperty(O, P)

1.Assert: Type(O) is Object.
2.Assert: IsPropertyKey(P) is true.
3.Let desc be O.[[GetOwnProperty]](P).
4.If desc is an abrupt completion, return desc
5.Set desc to desc.[[Value]]
6.If desc is undefined, return NormalCompletion(false).
7.Return NormalCompletion(true).

我们也可以在没有感叹号的情况下重写[[GetOwnProperty]]内部方法:

  O.[[GetOwnProperty]]

1.Let temp be OrdinaryGetOwnProperty(O, P).
2.Assert: temp is not an abrupt completion.
3.Let temp be temp.[[Value]].
4.Return NormalCompletion(temp).

在这里我们假设temp是一个全新的临时变量,不会与其他任何东西相冲突。

我们还获得了这样的知识,当一个return语句返回的东西不是一个Completion Record时,它会被隐式地包装在NormalCompletion之内。

另外:Return?Foo()

规范使用了Return?Foo()这种符号——为什么使用问号?

Return ? Foo()展开为:

1. Let temp be Foo().
2. If temp is an abrupt completion, return temp.
3. Set temp to temp.[[Value]].
4. Return NormalCompletion(temp).

这与Return Foo()是相同的;对于突然的和正常的完成,它的行为方式是一样的。

只有出于编程方式上原因,才使用Return?Foo(),以使其更明确地表示Foo返回一个完成记录。

断言

规范中的断言断定了算法的不变条件。它们是为了清晰逻辑而添加的,并不增加对实现的任何要求——实现不需要检查它们。

继续

抽象操作委托给其他抽象操作(参见下图),但是基于此博客文章,我们应该能够明确它们的工作原理。我们会遇到属性描述符,这只是另一种规范类型。

 Object.prototype.hasOwnProperty 的函数调用图

总结

我们阅读了一个简单的方法——Object.prototype.hasOwnProperty——和它调用的抽象操作。熟悉了与错误处理相关的简写?和!。我们还了解了语言类型、规范类型、内部槽和内部方法。