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 argumentV
, the following steps are taken:
- Let
P
be? ToPropertyKey(V)
.- Let
O
be? ToObject(this value)
.- Return
? HasOwnProperty(O, P)
.
…and…
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 argumentsO
andP
whereO
is the object andP
is the property key. This abstract operation performs the following steps:
- Assert:
Type(O)
isObject
.- Assert:
IsPropertyKey(P)
istrue
.- Let
desc
be? O.[[GetOwnProperty]](P)
.- If
desc
isundefined
, returnfalse
.- 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]]:
When the
[[GetOwnProperty]]
internal method ofO
is called with property keyP
, the following steps are taken:
- 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:
Name | Description |
---|---|
[[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:
- If
argument
is abrupt, returnargument
- Set
argument
toargument.[[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:
- Let
obj
beFoo()
. (obj
is a Completion Record.)ReturnIfAbrupt(obj)
.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:
- Let
val
beFoo()
.- Assert:
val
is not an abrupt completion.- Set
val
toval.[[Value]]
.
Using this knowledge, we can rewrite Object.prototype.hasOwnProperty
like this:
Object.prototype.hasOwnProperty(V)
- Let
P
beToPropertyKey(V)
.- If
P
is an abrupt completion, returnP
- Set
P
toP.[[Value]]
- Let
O
beToObject(this value)
.- If
O
is an abrupt completion, returnO
- Set
O
toO.[[Value]]
- Let
temp
beHasOwnProperty(O, P)
.- If
temp
is an abrupt completion, returntemp
- Let
temp
betemp.[[Value]]
- Return
NormalCompletion(temp)
…and we can rewrite HasOwnProperty
like this:
HasOwnProperty(O, P)
- Assert:
Type(O)
isObject
.- Assert:
IsPropertyKey(P)
istrue
.- Let
desc
beO.[[GetOwnProperty]](P)
.- If
desc
is an abrupt completion, returndesc
- Set
desc
todesc.[[Value]]
- If
desc
isundefined
, returnNormalCompletion(false)
.- Return
NormalCompletion(true)
.
We can also rewrite the [[GetOwnProperty]]
internal method without the exclamation mark:
O.[[GetOwnProperty]]
- Let
temp
beOrdinaryGetOwnProperty(O, P)
.- Assert:
temp
is not an abrupt completion.- Let
temp
betemp.[[Value]]
.- 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:
- Let
temp
beFoo()
.- If
temp
is an abrupt completion, returntemp
.- Set
temp
totemp.[[Value]]
.- 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.
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.
Useful links #
How to Read the ECMAScript Specification: a tutorial which covers much of the material covered in this post, from a slightly different angle.