Understanding the ECMAScript spec, part 1

Published · Tagged with ECMAScript Understanding ECMAScript

All episodes

In this article, we take a simple function in the spec and try to understand the notation. Let’s go!

Preface #

Even if you know JavaScript, reading its language specification, ECMAScript Language specification, or the ECMAScript spec for short, can be pretty daunting. At least that’s how I felt when I started reading it for the first time.

Let’s start with a concrete example and walk through the spec to understand it. The following code demonstrates usage of Object.prototype.hasOwnProperty:

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

In the example, o doesn’t have a property called hasOwnProperty, so we walk up the prototype chain and look for it. We find it in o’s prototype, which is Object.prototype.

To describe how Object.prototype.hasOwnProperty works, the spec uses pseudocode-like descriptions:

Object.prototype.hasOwnProperty(V)

When the hasOwnProperty method is called with argument V, the following steps are taken:

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

…and…

HasOwnProperty(O, P)

The abstract operation HasOwnProperty is used to determine whether an object has an own property with the specified property key. A Boolean value is returned. The operation is called with arguments O and P where O is the object and P is the property key. This abstract operation performs the following steps:

  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.

But what’s an “abstract operation”? What are the things inside [[ ]]? Why is there a ? in front of a function? What do the asserts mean?

Let’s find out!

Language types and specification types #

Let’s start with something that looks familiar. The spec uses values such as undefined, true, and false, which we already know from JavaScript. They are all language values, values of language types which the spec also defines.

The spec also uses language values internally, for example, an internal data type might contain a field whose possible values are true and false. In contrast, JavaScript engines don’t typically use language values internally. For example, if the JavaScript engine is written in C++, it would typically use the C++ true and false (and not its internal representations of the JavaScript true and false).

In addition to language types, the spec also uses specification types, which are types that occur only in the spec, but not in the JavaScript language. The JavaScript engine does not need to (but is free to) implement them. In this blog post, we'll get to know the specification type Record (and its subtype Completion Record).

Abstract operations #

Abstract operations are functions defined in the ECMAScript spec; they are defined for the purpose of writing the spec concisely. A JavaScript engine doesn’t have to implement them as separate functions inside the engine. They cannot be directly called from JavaScript.

Internal slots and internal methods #

Internal slots and internal methods use names enclosed in [[ ]].

Internal slots are data members of a JavaScript object or a specification type. They are used for storing the state of the object. Internal methods are member functions of a JavaScript object.

For example, every JavaScript object has an internal slot [[Prototype]] and an internal method [[GetOwnProperty]].

Internal slots and methods are not accessible from JavaScript. For example, you cannot access o.[[Prototype]] or call o.[[GetOwnProperty]](). A JavaScript engine can implement them for their own internal use, but doesn’t have to.

Sometimes internal methods delegate to similarly-named abstract operations, such as in the case of ordinary objects' [[GetOwnProperty]]:

[[GetOwnProperty]](P)

When the [[GetOwnProperty]] internal method of O is called with property key P, the following steps are taken:

  1. Return ! OrdinaryGetOwnProperty(O, P).

(We’ll find out what the exclamation mark means in the next chapter.)

OrdinaryGetOwnProperty is not an internal method, since it’s not associated with any object; instead, the object it operates on is passed as a parameter.

OrdinaryGetOwnProperty is called “ordinary” since it operates on ordinary objects. ECMAScript objects can be either ordinary or exotic. Ordinary objects must have the default behavior for a set of methods called essential internal methods. If an object deviates from the default behavior, it’s exotic.

The most well-known exotic object is the Array, since its length property behaves in a non-default way: setting the length property can remove elements from the Array.

Essential internal methods are the methods listed here.

Completion records #

What about the question marks and exclamation marks? To understand them, we need to look into Completion Records!

Completion Record is a specification type (only defined for spec purposes). A JavaScript engine doesn’t have to have a corresponding internal data type.

A Completion Record is a “record” — a data type which has a fixed set of named fields. A Completion Record has three fields:

NameDescription
[[Type]]One of: normal, break, continue, return, or throw. All other types except normal are abrupt completions.
[[Value]]The value that was produced when the completion occurred, for example, the return value of a function or the exception (if one is thrown).
[[Target]]Used for directed control transfers (not relevant for this blog post).

Every abstract operation implicitly returns a Completion Record. Even if it looks like an abstract operation would return a simple type such as Boolean, it’s implicitly wrapped into a Completion Record with the type normal (see Implicit Completion Values).

Note 1: The spec is not fully consistent in this regard; there are some helper functions which return bare values and whose return values are used as is, without extracting the value from the Completion Record. This is usually clear from the context.

Note 2: The spec editors are looking into making the Completion Record handling more explicit.

If an algorithm throws an exception, it means returning a Completion Record with [[Type]] throw whose [[Value]] is the exception object. We’ll ignore the break, continue and return types for now.

ReturnIfAbrupt(argument) means taking the following steps:

  1. If argument is abrupt, return argument
  2. Set argument to argument.[[Value]].

That is, we inspect a Completion Record; if it’s an abrupt completion, we return immediately. Otherwise, we extract the value from the Completion Record.

ReturnIfAbrupt might look like a function call, but it’s not. It causes the function where ReturnIfAbrupt() occurs to return, not the ReturnIfAbrupt function itself. It behaves more like a macro in C-like languages.

ReturnIfAbrupt can be used like this:

  1. Let obj be Foo(). (obj is a Completion Record.)
  2. ReturnIfAbrupt(obj).
  3. Bar(obj). (If we’re still here, obj is the value extracted from the Completion Record.)

And now the question mark comes into play: ? Foo() is equivalent to ReturnIfAbrupt(Foo()). Using a shorthand is practical: we don’t need to write the error handling code explicitly each time.

Similarly, Let val be ! Foo() is equivalent to:

  1. Let val be Foo().
  2. Assert: val is not an abrupt completion.
  3. Set val to val.[[Value]].

Using this knowledge, we can rewrite Object.prototype.hasOwnProperty like this:

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)

…and we can rewrite HasOwnProperty like this:

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).

We can also rewrite the [[GetOwnProperty]] internal method without the exclamation mark:

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).

Here we assume that temp is a brand new temporary variable which doesn’t collide with anything else.

We’ve also used the knowledge that when a return statement returns something else than a Completion Record, it’s implicitly wrapped inside a NormalCompletion.

Side track: Return ? Foo() #

The spec uses the notation Return ? Foo() — why the question mark?

Return ? Foo() expands to:

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

Which is the same as Return Foo(); it behaves the same way for both abrupt and normal completions.

Return ? Foo() is only used for editorial reasons, to make it more explicit that Foo returns a Completion Record.

Asserts #

Asserts in the spec assert invariant conditions of the algorithms. They are added for clarity, but don't add any requirements to the implementation — the implementation doesn’t need to check them.

Moving on #

The abstract operations delegate to other abstract operations (see picture below), but based on this blog post we should be able to figure out what they do. We’ll encounter Property Descriptors, which is just another specification type.

Function call graph starting from Object.prototype.hasOwnProperty

Summary #

We read through a simple method — Object.prototype.hasOwnProperty — and abstract operations it invokes. We familiarized ourselves with the shorthands ? and ! related to error handling. We encountered language types, specification types, internal slots, and internal methods.

How to Read the ECMAScript Specification: a tutorial which covers much of the material covered in this post, from a slightly different angle.