How JavaScript Accesses Properties on Primitives: An ECMAScript Spec Walkthrough

This article explores how JavaScript’s ECMAScript specification defines property access on primitive values, detailing the runtime semantics of member expressions, abstract operations like GetValue and ToObject, internal slots, records, and the behavior of methods such as String.prototype.substring when invoked with various receivers.

Node Underground
Node Underground
Node Underground
How JavaScript Accesses Properties on Primitives: An ECMAScript Spec Walkthrough

In the previous part we learned how to start reading the ECMAScript specification; this part dives deeper by examining how JavaScript accesses properties on primitive values at runtime.

Basic Type Property Access

Member properties on objects are looked up via the prototype chain, e.g. ({}).hasOwnProperty works because the object literal’s prototype is Object.prototype. Primitive values also expose properties—where are those defined? Do primitives have prototypes?

'foobar'.substring(3); // -> 'bar'
Note: The article contains many direct quotations from the ECMAScript spec as of March 10, 2020; newer versions may have updates.

Where Is Member‑Expression Syntax Defined?

The grammar for MemberExpression includes seven productions, such as MemberExpression [ Expression ] (e.g., obj['foo']) and MemberExpression . IdentifierName (e.g., 'foobar'.substring).

MemberExpression:
    PrimaryExpression
    MemberExpression [ Expression ]
    MemberExpression . IdentifierName
    MemberExpression TemplateLiteral
    SuperProperty
    MetaProperty
    new MemberExpression Arguments

To understand how primitive property access works, we examine the runtime semantics of these productions.

For more context on context‑free grammars, see https://en.wikipedia.org/wiki/Context-free_grammar.

Runtime Semantics of Member Expressions

The runtime algorithm for MemberExpression . IdentifierName is:

MemberExpression : MemberExpression . IdentifierName
1. Let baseReference be the result of evaluating MemberExpression.
2. Let baseValue be ? GetValue(baseReference).
3. Determine strict mode flag.
4. Return ? EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict).

Step 4 delegates to the abstract operation EvaluatePropertyAccessWithIdentifierKey:

EvaluatePropertyAccessWithIdentifierKey(baseValue, identifierName, strict)
1. Assert: identifierName is an IdentifierName.
2. Let bv be ? RequireObjectCoercible(baseValue).
3. Let propertyNameString be StringValue of identifierName.
4. Return a Reference whose base is bv, name is propertyNameString, and strict flag is strict.

This yields a Reference object; the actual value is obtained later via GetValue. For example, after evaluating 'foobar'.startsWith, a subsequent call like 'foobar'.startsWith('foo') is performed.

Reference types are used in operations such as delete , typeof , assignment, and super . An assignment like obj.foo = 'bar' first creates a Reference for obj.foo , which is later resolved to a concrete value.

Records & Completion Records

The spec uses the ? marker to indicate that an operation may produce an abrupt completion (throw, return, break, etc.). Completion Records capture the type ( [[Type]]), value ( [[Value]]), and optional target label ( [[Target]]). Normal completions have [[Type]] set to normal; abrupt completions include throw, return, break, and continue.

Handling abrupt completions traditionally required four steps; newer spec versions introduce ReturnIfAbrupt to simplify the pattern.

1. Let result be AbstractOp().
2. ReturnIfAbrupt(result).
3. result is the result we need.

The ? operator now implicitly performs ReturnIfAbrupt, automatically unwrapping the [[Value]] of a normal completion.

Object Internal Slots

Objects have internal methods such as [[Get]], [[Set]], [[GetPrototypeOf]], [[GetOwnProperty]], and [[Delete]]. Functions are callable objects with additional internal methods [[Call]] and optionally [[Construct]]. Most objects also have the internal slot [[Prototype]], which is accessed via the [[GetPrototypeOf]] method. Proxy objects, for example, lack a direct [[Prototype]] slot but implement [[GetPrototypeOf]] to delegate to their handler.

Details on ordinary object internal methods are in section 9.1; proxy internal methods are in section 9.5 of the spec.

Conversion of Primitives with GetValue

When GetValue encounters a primitive base, step 5.a converts it to an object via the abstract operation ToObject. The conversion rules are:

Undefined or Null → TypeError.

Boolean → new Boolean object with [[BooleanData]] set to the primitive value.

Number → new Number object with [[NumberData]] set.

String → new String object with [[StringData]] set.

Symbol → new Symbol object with [[SymbolData]] set.

BigInt → new BigInt object with [[BigIntData]] set.

Object → returns the argument unchanged.

After conversion, property access proceeds via the object's [[Get]] method, which ultimately calls the abstract operation OrdinaryGet to walk the prototype chain.

OrdinaryGet(O, P, Receiver)
1. Assert: IsPropertyKey(P) is true.
2. Let desc be ? O.[[GetOwnProperty]](P).
3. If desc is undefined, then
   a. Let parent be ? O.[[GetPrototypeOf]]().
   b. If parent is null, return undefined.
   c. Return ? parent.[[Get]](P, Receiver).
4. If IsDataDescriptor(desc) is true, return desc.[[Value]].
5. Assert: IsAccessorDescriptor(desc) is true.
6. Let getter be desc.[[Get]].
7. If getter is undefined, return undefined.
8. Return ? Call(getter, Receiver).

Property descriptors themselves are Records, typically expressed in JavaScript as object literals passed to Object.defineProperty.

const it = 'foobar';
'foobar'.substring(3); // -> 'bar'

Summarizing the example, the primitive string 'foobar' is first converted to a String object, its String.prototype is consulted for substring, and the method is invoked with the newly created object as the receiver, yielding 'bar'.

String.prototype.substring

If String.prototype.substring is called with undefined as the receiver (e.g., String.prototype.substring.call(undefined, 2, 4)), the spec defines the outcome; readers can consult ECMAScript 21.1.3.22 for the exact result.

More Links

TC39, ECMAScript® 2020 Language Specification – Draft, March 10 2020, https://tc39.es/ecma262

Timothy Gu, How to Read the ECMAScript Specification, https://timothygu.me/es-howto

Marja Hölttä, Understanding the ECMAScript spec, part 1, https://v8.dev/blog/understanding-ecmascript-part-1

Marja Hölttä, Understanding the ECMAScript spec, part 2, https://v8.dev/blog/understanding-ecmascript-part-2

ECMAScriptSpecificationprimitivesproperty access
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.