如何读 ECMAScript 规范(二)

  1. 参考
  2. 运行时语义
    1. 算法步骤
    2. 抽象的操作
    3. 什么是 [[This]]
      1. 记录 & 字段
      2. JavaScript 对象的内部插槽
      3. JavaScript 对象的内部方法
    4. 完成记录;? 和!

参考

本文翻译自:

https://timothygu.me/es-howto/

运行时语义

语言和 api 的运行时语义是规范中最重要的部分,通常是人们最关心的问题。

总的来说,在规范中阅读这些章节是非常简单的。然而,该规范使用了大量的速记,这些速记刚刚开始 (至少对我来说) 是相当讨厌的。我将尝试解释其中的一些约定,然后将它们应用到一个通常的工作流程中,以弄清楚几件事情是如何工作的。

算法步骤

ECMAScript 中的大多数运行时语义是由一系列算法步骤指定的,与伪代码没有什么不同,但形式精确得多。

image

抽象的操作

您有时会在规范中看到调用类似函数的内容。

Boolean()函数:

image

ToBoolean”函数被称为抽象的操作。抽象的原因是它实际上并没有作为 JavaScript 代码的函数公开。它只是一个规范作者发明的符号。

抽象操作

什么是 [[This]]

有时,您可能会看到 [[符号 ]]被使用,例如 “Let proto be obj.[[Prototype]]“。

从技术上讲,这种表示法可以表示几种不同的东西,这取决于它出现的上下文。要理解这种表示法指的是一些无法通过 JavaScript 代码观察到的内部属性。

确切地说,它可能意味着三种不同的东西,我将用规范中的示例来说明。但是,现在请随意跳过它们。

记录 & 字段

ECMAScript 规范使用术语 Record 来指代具有一组固定键的键值映射 – 有点像类 C 语言中的 ** 结构体 **。

记录(Record)的每个键值对(key-value)称为一个 ** 字段 **(field)。由于记录只能出现在规范中,而不能出现在实际的 JavaScript 代码中,因此使用 [[Notation]] 来引用记录 (Record) 的字段(field)。

值得注意的是,属性描述符还建模为具有 field: [[Value]]、[[Wwriteable]]、[[Get]]、[[Set]]、[[Enumerable]]和 [[Configurable]]的 Record。IsDataDescriptor 抽象操作广泛使用以下表示法:

当使用属性描述符 Desc 调用抽象操作 IsDataDescriptor 时,将执行以下步骤:

  1. 如果 Desc 是 undefined,返回 false
  2. 如果 Desc.[[Value]]Desc.[[Writeable]] 缺失,返回 false
  3. 返回 true

JavaScript 对象的内部插槽

JavaScript 对象可能具有所谓的内部插槽,规范使用这些插槽来保存其中的数据。

与记录字段一样,这些内部插槽也无法使用 JavaScript 进行观察,但其中一些可以通过特定于实现的工具(如 Google Chrome 的 DevTools)公开。因此,使用 [[记号]] 来描述内部插槽也是有意义的。

内部插槽的细节将在 § 2.5 JavaScript Objects 中介绍。现在,不要太担心它们的用途,但请注意以下示例。

大多数 JavaScript 对象有一个内部插槽 [[Prototype]],引用了它们所继承的对象。这个内部插槽的值通常是 Object.getProrotypeOf() 方法的返回值。在 OrdinaryGetPrototypeOf 抽象操作中,访问此内部插槽的值:

当使用对象 O 调用抽象操作 OrdinaryGetPrototypeOf 时,将执行以下步骤:

  1. 返回 O.[[Prototype]]

注: “对象” 和 “记录” 字段的内部槽在外观上是相同的,但可以通过查看记号(点运算符之前的部分)来消除歧义,无论是对象还是记录。可以在上下文中看出。

JavaScript 对象的内部方法

JavaScript 对象也可能具有所谓的内部方法。与内部插槽一样,这些内部方法无法通过 JavaScript 直接观察到。

因此,使用 [[记号]] 来描述内部方法也是有意义的。

内部方法的细节将在 § 2.5 JavaScript Objects 中介绍。现在,不要太担心它们的用途,但请注意以下示例。

所有的 JavaScript 函数都有一个内部方法 [[Call]],它运行函数。Call 抽象操作

完成记录;? 和!

ECMAScript 规范中的每个运行时语义都显式或隐式返回一个报告其结果的完成记录(Completion Records)。此完成记录是具有三个可能字段的记录(Record):

  • 一个 [[Type]] (normal, return,throw,break,contintue)
  • 如果 [[Type]] 是 normal,return 或者 throw,它可以额外有一个 [[Value]] (返回 / 抛出了什么)
  • 如果 [[Type]] 是 break 或者 continue,则它可以选择携带一个称为 [[Target]] 的标签,脚本执行会因为此运行时语义而中断 / 继续执行该标签。

    注意:[[]] 也用来表示记录 (Records) 的字段(fields)。

[[Type]] 为 normal 时的完成记录称为 *normal 完成 *。其他 [[Type]] 的完成记录称为 *abrupt 完成 *

大多数时候,你只会与 [[Type]] 为 throw 的 abrupt 完成 ** 打交道。其他三种(return,break,continue)abrupt 完成 ** 类型仅用于查看如何评估特定语法元素。

事实上,您将永远不会在内置函数的定义中看到 normal 和任何其他类型,因为中断 / 继续 / 返回不能跨函数边界工作。

规范连接

由于完成记录的定义,JavaScript 中的细微差别,如冒泡错误,直到尝试捕获块在规范中不存在。

事实上,错误(或更确切地说是 abrupt 完成)是显式处理的。

在没有任何速记的情况下,对抽象操作的普通调用的规范文本可能返回计算结果或引发错误,如下所示:

调用抽象操作的几个步骤,该操作可能在没有任何速记的情况下抛出:

  1. 设 resultCompletionRecord 是一个 AbstractOp()。
    1. 注意,resultCompletionRecord 是一个完成记录
  2. 如果 resultCompletionRecord 是一个 abrupt 记录,返回 resultCompletionRecord
    1. 注意,这里,resultCompletionRecord 如果是 abrupt 完成,直接返回。换句话说,将在 AbstractOp 中引发错误,并中止其余步骤。
  3. 设 result 是 resultCompletionRecord.[[Value]]
    1. 注意,在确保我们获得正常完成之后,我们现在可以解开完成记录的包装,以获得我们需要的计算的实际结果。
  4. result 就是我们需要的结果。

这可能会让你想起 C 中的手动错误处理:

int result = abstractOp();              // Step 1
if (result < 0)                         // Step 2
  return result;                        // Step 2 (continued)
                                        // Step 3 is unneeded
// func() succeeded; carrying on...     // Step 4

但是为了减少这些步骤,ECMAScript 规范的编辑器添加了一些速记(shorthands)。

自 ES2016 起,相同的规范文本可以改为以以下两种等效方式编写:

调用抽象操作的几个步骤可能抛出 ReturnIfAbrupt

  1. 设 result 是 AbstractOp()。
    1. 注意,这里,就像上面的 step1,result 是一个完成记录
  2. ReturnIfAbrupt(result)
    1. 注意,ReturnIfAbrupt 通过转发来处理任何可能的 abrupt 完成,并自动将结果解包到其 [[Value]] 中。
  3. result 就是我们想要的结果。

或者,更简洁地说,用一个特殊的问号(?)表示法:

调用抽象操作的几个步骤可能抛出 问号标记(?)

  1. 设 result 是?AbstractOp()
    1. 注意:在此表示法中,我们根本不处理完成记录。这个?速记为我们处理一切,结果可以立即使用。
  2. result 就是我们想要的结果。

有时,如果我们知道对 AbstractOp 的特定调用永远不会返回 abrupt 完成,它可以向读者传达有关规范意图的更多信息。在这些情况下,使用感叹号 (!)

调用抽象操作的几个步骤永远不抛出的 感叹号标记(!)

  1. 设 result 是! AbstractOp()
    1. 注意,?可能会带来任何错误,而!断言我们这次调用永远不会得到任何 abrupt 完成,如果我们这样做,那将是规范中的一个错误。就像?的情况,我们完全不处理完成记录。result 在这之后是立即可用的。
  2. result 就是我们想要的结果。

注意,! 看起来像一个有效的 JavaScript 表达式,可能会变得非常混乱:!ToBoolean(value)

在这里,!只是意味着我们确信对 ToBoolean 的调用永远不会返回异常,而不是布尔取反。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论。
我的空间