cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

A tour of the abstract operations

Follow JavaScript's named specification rules for converting objects, comparing values, stepping through iterators, and choosing array results.

By the end, you can
  • 01
    Trace a conversionPredict which object hook runs and distinguish primitive, property-key, and numeric conversions.
  • 02
    Explain comparisonsFollow the different paths taken by loose equality and relational comparison, including visible conversion order.
  • 03
    Read iteration and constructionExplain iterator results, early closing, and how an array method chooses its result constructor.

One rule used in many places

How to read ECMAScript introduced named rules inside the language standard. Now we follow those rules through code you can actually run. An abstract operation is a reusable rule in the specification, not a global function exposed to your program.

An object acts like a pricePop out in the code editor (opens in a new tab)JavaScript
const price = { valueOf() { return 3; } };
console.log(price + 2);

Line 1 makes a price object whose valueOf returns 3. Line 2 adds 2 and prints 5. The interesting step is hidden between those lines: addition first asks the object for a primitive value. A primitive is a non-object value such as a Number, String, or BigInt.

Definition

An abstract operation gives a name to a required piece of language behavior. To understand one, start with the JavaScript expression, then follow only the spec operations needed to explain its observable result.

The previous lesson, Internal methods & exotic objects, explained how objects respond to property access. Here we ask what happens when those objects are used as keys, numbers, comparison operands, iterables, or constructors for derived arrays. The specification describes the result, not the engine's machine code.

ToPrimitive and ordinary conversion

ToPrimitive asks for a non-object value. With no special hook, it delegates to OrdinaryToPrimitive. A hint tells the conversion which kind of result to try first; it does not force the returned value to have that type.

The first primitive resultPop out in the code editor (opens in a new tab)JavaScript
const price = { valueOf() { return 3; } };
console.log(price + 2);

Line 1 defines valueOf to return the Number 3. Line 2 prints 5 because the default conversion for this ordinary object tries valueOf first. Addition then adds two Numbers. See the exact ToPrimitive rule for the hook and fallback paths.

If valueOf returns an object, try toStringPop out in the code editor (opens in a new tab)JavaScript
const calls = [];
const price = {
  valueOf() { calls.push("valueOf"); return {}; },
  toString() { calls.push("toString"); return "3"; }
};
console.log(Number(price), calls.join(","));

Line 1 starts an empty call log. Line 3 logs valueOf but returns another object, so it cannot finish primitive conversion. Line 4 logs toString and returns the String 3. Line 6 calls Number on the result and prints 3 valueOf,toString. Both methods must fail to produce a primitive before OrdinaryToPrimitive throws a TypeError; it does not accept an object as the final answer.

Real-life analogyA phone contact list

Your phone stores more about a contact than just a name. To search, you first need a name you can type.

In real life: A contact has details
In JavaScript: An object has properties
In real life: A saved name identifies it
In JavaScript: A primitive can name a key
In real life: The phone tries a saved name
In JavaScript: Conversion tries an available method

Where the analogy stops: An object can define conversion hooks and return Symbols; a phone contact lookup cannot model those language rules.

Choosing a conversion hint

The order changes when a string result is preferred. String(object) tries toString before valueOf in the ordinary fallback. For a number hint, OrdinaryToPrimitive tries them in the other order. A method returning an object causes the other candidate to be tried.

String hint chooses toString firstPop out in the code editor (opens in a new tab)JavaScript
const calls = [];
const key = {
  valueOf() { calls.push("valueOf"); return 3; },
  toString() { calls.push("toString"); return "tea"; }
};
console.log(String(key), calls.join(","));

Line 1 creates the log. Line 3 could return 3, but line 4's toString runs first with a string hint. Line 6 prints tea toString. Nothing called valueOf this time.

The explicit conversion hook receives a hintPop out in the code editor (opens in a new tab)JavaScript
const price = {
  [Symbol.toPrimitive](hint) {
    console.log(hint);
    return 3;
  }
};
console.log(price + 2);
console.log(Number(price));

Lines 2 through 5 define Symbol.toPrimitive. Line 7 prints default and then 5: addition requested a primitive without a preferred type. Line 8 prints number and then 3 because Number(price) requests numeric conversion. The hook is tried before OrdinaryToPrimitive; if it returns another object, conversion throws rather than trying valueOf afterward.

Step through object conversion
Step 0 of 7Ready
Your turn: follow the blue line

Replay of instrumented JavaScript example code, not an engine debugger.

Running in
  1. script
Next: line 4
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function total(price) {  return price + 2;}  [Symbol.toPrimitive](hint) {    console.log(hint);    return 3;  }};console.log(total(price));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Step through the instrumented total function to see the hook receive default, return 3, and let addition return 5. The replay records actual calls of lesson code; it is not an engine debugger. This is a useful way to find which part of a surprising expression has side effects.

ToPropertyKey makes a usable name

ToPropertyKey converts a value to a String or a Symbol. It asks ToPrimitive for a string hint, keeps a Symbol primitive as it is, and converts any other primitive to a String. This is why a Number written between brackets can name a property stored under a String.

Two spellings of one propertyPop out in the code editor (opens in a new tab)JavaScript
const cart = { 3: "tea" };
console.log(cart[3], cart["3"]);

Line 1 stores tea under the String key "3". Line 2 reads it with both 3 and "3", then prints tea tea. A numeric index on an ordinary object does not create a separate Number-keyed property.

A computed key asks for stringPop out in the code editor (opens in a new tab)JavaScript
const cart = {};
const key = {
  [Symbol.toPrimitive](hint) {
    console.log(hint);
    return 3;
  }
};
cart[key] = "tea";
console.log(cart["3"]);

Line 1 creates the cart. Lines 2 through 7 create a key object that logs its hint and returns 3. Line 8 prints string during assignment; the primitive Number becomes the String key "3". Line 9 prints tea when reading it back. The current ToPropertyKey steps make that path explicit.

A Symbol remains its own keyPop out in the code editor (opens in a new tab)JavaScript
const key = Symbol("tea");
const cart = { [key]: 3 };
console.log(cart[key], cart["Symbol(tea)"]);

Line 1 creates one Symbol. Line 2 stores 3 under it. Line 3 prints 3 undefined: the Symbol key and the String "Symbol(tea)" are different. A description helps humans identify a Symbol; it is not a substitute for the Symbol as a key.

ToNumeric preserves BigInt

ToNumeric requests a primitive with a number hint, then returns it unchanged if it is a BigInt. Otherwise it applies ToNumber. This named rule explains why a numeric operator can work with BigInt without silently changing it into an imprecise Number.

Increment a BigInt-valued objectPop out in the code editor (opens in a new tab)JavaScript
let price = { valueOf() { return 3n; } };
console.log(typeof ++price, price === 4n);

Line 1 creates an object whose valueOf returns 3n. Line 2 increments the variable after conversion, then prints bigint true: the incremented value is 4n. Unlike a read-only addition, prefix increment also assigns the result back to price. The ToNumeric steps preserve the BigInt branch.

Comparison and addition do different thingsPop out in the code editor (opens in a new tab)JavaScript
console.log(3n < 4);
try { console.log(3n + 4); }
catch (error) { console.log(error.name); }

Line 1 prints true because relational comparison can compare BigInt and Number values. Line 2 tries to add 3n to 4; line 3 catches the TypeError and prints TypeError. ToNumeric does not mean every operator accepts mixed numeric types.

Change the price limit
The comparison being changedPop out in the code editor (opens in a new tab)JavaScript
function comparePrice(input) {  const price = { valueOf() { return 3; } };  return price < input;}console.log(comparePrice(4));
Observed result
true
Try it yourself

The object supplies 3. Comparing 3 < 4 prints true. Reset restores the limit to 4.

This runs the same comparison function as the example. It is not an engine debugger.

Change only the limit. The visible function converts an object that supplies 3 before comparing it to your selected Number; Reset restores 4 and true. It is the actual JavaScript comparison, not a simulated interpreter for ToNumeric.

IsLooselyEqual and IsLessThan ask different questions

IsLooselyEqual defines == using cases for pairs of types. A Number versus a String converts the String to Number; an object versus a Number or String converts the object to a primitive. === follows the separate IsStrictlyEqual rule and does not coerce different types.

Equality with and without conversionPop out in the code editor (opens in a new tab)JavaScript
console.log("3" == 3);
console.log("3" === 3);

Line 1 converts "3" for loose equality and prints true. Line 2 uses strict equality and prints false, because String and Number are different types. For a full introduction to these operators, see Equality & sameness.

An object is not converted in every equality casePop out in the code editor (opens in a new tab)JavaScript
const price = { valueOf() { console.log("valueOf"); return 3; } };
console.log(price == 3);
console.log(price == null);

Line 1 gives price a logging valueOf method. Line 2 prints valueOf, then true, because comparison with 3 asks for a primitive. Line 3 prints false without another conversion: an ordinary object compared with null matches neither the null/undefined special case nor an object-to-primitive case. The browser's special legacy document.all case is not an ordinary object and is not part of this example.

IsLessThan describes relational comparison. If both converted values are Strings, it compares their UTF-16 code units in order; otherwise it follows numeric paths, with special BigInt/String cases. It can return a spec-level undefined for unordered inputs such as NaN; the JavaScript comparison operators turn that into a Boolean result.

String order is not numeric orderPop out in the code editor (opens in a new tab)JavaScript
console.log("10" < "2");
console.log("10" < 2);

Line 1 prints true: the String "10" begins with a code unit that comes before the one in "2". Line 2 prints false: a String and Number are compared numerically, and ten is not less than two. Do not use a lexical comparison as a price sorter.

Conversion still happens left to rightPop out in the code editor (opens in a new tab)JavaScript
const left = { valueOf() { console.log("left"); return 3; } };
const right = { valueOf() { console.log("right"); return 4; } };
console.log(left < right);
console.log(left > right);

Lines 1 and 2 define logging operands. Line 3 prints left, right, then true. Line 4 prints left, right, then false. The IsLessThan leftFirst argument preserves this visible order even when > describes its test by passing operands in the other order.

NaN is unorderedPop out in the code editor (opens in a new tab)JavaScript
console.log(NaN < 3, NaN >= 3);

Line 1 prints false false. Neither less-than nor greater-than-or-equal makes NaN comparable; the spec's internal undefined comparison result is handled as false by both JavaScript operators. Read the full cases in IsLooselyEqual and IsLessThan when a real input differs from these small examples.

Comparison rules with different jobs
RuleQuestionImportant difference
IsLooselyEqualThe == comparisonConverts by type pair, including object versus primitive
IsStrictlyEqualThe === comparisonDifferent types cannot compare equal
IsLessThanThe < comparison; also helps define >Primitive conversion order is controlled by leftFirst

GetIterator and IteratorStep move through values

An iterable provides Symbol.iterator. GetIterator calls that method in the synchronous case and creates an Iterator Record holding the iterator object, its next method, and its done state. A loop then asks the iterator for successive result objects.

An ordinary array is iterablePop out in the code editor (opens in a new tab)JavaScript
const items = ["tea", "milk"];
for (const item of items) console.log(item);

Line 1 creates an array of tea and milk. Line 2 uses for-of to print tea and then milk. Each iteration requests another result until its done field indicates completion.

Watch next and early returnPop out in the code editor (opens in a new tab)JavaScript
const items = {
  [Symbol.iterator]() {
    console.log("iterator");
    let index = 0;
    return {
      next() {
        console.log("next");
        return index++ === 0 ? { value: "tea", done: false } : { done: true };
      },
      return() { console.log("return"); return { done: true }; }
    };
  }
};
for (const item of items) { console.log(item); break; }

Line 2 defines the iterator method and line 3 logs iterator. Line 6's next method logs next; line 8 returns a result containing tea with done false on the first call. Line 14 prints tea, then breaks. Line 10's return method logs return during the early close. The output is iterator, next, tea, return.

IteratorStep calls next and checks done. It returns the whole result object or the spec marker DONE, not its value. IteratorStepValue additionally reads the value if not done. These names are different operations in the current spec. A normal exhausted iterator need not call return, unlike an early break.

Exhaustion does not call returnPop out in the code editor (opens in a new tab)JavaScript
const items = {
  [Symbol.iterator]() {
    let count = 0;
    return {
      next() { console.log("next"); return count++ ? { done: true } : { value: "tea", done: false }; },
      return() { console.log("return"); return { done: true }; }
    };
  }
};
console.log([...items].join(","));

Line 1 makes an iterable. Line 5 logs next for each request. Line 6 defines a return method, but line 10 spreads the whole iterable, so it prints next, next, then tea. The spread completes normally; it does not call return.

A finished result does not need its valuePop out in the code editor (opens in a new tab)JavaScript
const items = {
  [Symbol.iterator]() {
    return { next() { return { get done() { console.log("done"); return true; }, get value() { console.log("value"); return "tea"; } }; } };
  }
};
console.log([...items].length);

Line 3 returns a result with logging getters for done and value. Line 6 spreads the iterable and prints 0. The other output is only done: once the result is finished, the value getter is never read. This is direct evidence for the step-then-value distinction.

Step through an early iterator exit
Step 0 of 8Ready
Your turn: follow the blue line

Replay of instrumented JavaScript iterator code, not an engine debugger.

Running in
  1. script
Next: line 7
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function first(items) {  for (const item of items) {    console.log(item);    break;  }}  [Symbol.iterator]() {    console.log("iterator");    return {      next() { console.log("next"); return { value: "tea", done: false }; },      return() { console.log("return"); return { done: true }; }    };  }};first(items);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay runs first with a real iterable. Step through the iterator-method call, next result, value binding, and early return. It is a replay of instrumented example code, not a display of engine frames.

The iterator operations are not interchangeable
OperationWorkResult
GetIteratorFind and call the iterator methodProduces an Iterator Record, not an Array
IteratorStepCall next and inspect doneReturns a result object or the spec marker DONE
IteratorStepValueStep, then read value when not doneReturns a value or DONE; it is not an alias for IteratorStep
IteratorCloseAsk return to run on early exitA normal exhaustion does not call return

SpeciesConstructor chooses a derived result

SpeciesConstructor is a spec rule that reads an object's constructor and that constructor's Symbol.species property. When an operation uses this rule, it chooses a constructor for a new related object. Some Array methods such as map use an Array-specific species creation path with the same idea; not every method or new copy uses species.

A subclass normally stays a subclassPop out in the code editor (opens in a new tab)JavaScript
class Orders extends Array {}
const orders = new Orders("tea", "milk");
console.log(orders.map(item => item.toUpperCase()) instanceof Orders);

Line 1 makes an Array subclass. Line 2 creates an Orders instance. Line 3 maps it to a new array and prints true: the inherited default species returns the subclass constructor in this case.

Choose Array for a mapped copyPop out in the code editor (opens in a new tab)JavaScript
class Orders extends Array {
  static get [Symbol.species]() {
    console.log("species");
    return Array;
  }
}
const orders = new Orders("tea", "milk");
const copy = orders.map(item => item.toUpperCase());
console.log(copy instanceof Orders, copy instanceof Array, copy.join(","));

Lines 2 through 5 replace the species choice with Array and log species when read. Line 7 creates an Orders input. Line 8 maps its elements. Line 9 prints false true TEA,MILK: the output is a plain Array, not Orders. This concerns the new result, not the type of the original instance.

A null species falls back to ArrayPop out in the code editor (opens in a new tab)JavaScript
class Orders extends Array {
  static get [Symbol.species]() { return null; }
}
const orders = new Orders("tea");
console.log(orders.slice() instanceof Orders);

Line 2 returns null for species. Line 4 creates an Orders input. Line 5 prints false because the default Array constructor makes the slice result. In the general SpeciesConstructor rule, undefined or null species selects the supplied default constructor; an invalid non-constructor throws.

A non-constructor species is rejectedPop out in the code editor (opens in a new tab)JavaScript
class Orders extends Array {
  static get [Symbol.species]() { return 3; }
}
try { new Orders("tea").map(item => item); }
catch (error) { console.log(error.name); }

Line 2 supplies the Number 3 instead of a constructor. Line 4 asks map for a new result; line 5 catches the error and prints TypeError. In application code, prefer an explicit Array conversion unless you intentionally need subclassed results. Species is a compatibility feature, not a general factory hook for all objects.

Trace a checkout bug

Suppose a checkout page receives a computed field from a form helper. A plain String key is easy to inspect, but an object key can run application code during conversion. Reduce the case to a cart and one field before blaming the framework or the browser.

A computed cart fieldPop out in the code editor (opens in a new tab)JavaScript
const cart = { tea: 3 };
const field = { toString() { return "tea"; } };
console.log(cart[field]);

Line 1 creates a cart with tea priced at 3. Line 2 creates a field object whose toString returns tea. Line 3 prints 3. ToPropertyKey first asks for a primitive using a string hint; the converted key then finds cart.tea. This is an observable lookup, not a call to a global function named ToPropertyKey.

Stop after the first cart itemPop out in the code editor (opens in a new tab)JavaScript
const cart = ["tea", "milk"];
for (const item of cart) {
  if (item === "tea") { console.log(item); break; }
}

Line 1 creates a cart array. Line 2 begins iteration. Line 3 prints tea and breaks immediately, so milk is not printed. If your real cart is a custom iterable, test its next and return methods to see what cleanup an early exit performs.

For more conversion examples, visit Type coercion. For the broader comparison rules, visit Equality & sameness. Here the goal is to find the named operation that explains one surprising step, and then verify the result with a tiny program.

Which operation answers the question?
  • Object becomes a primitive for addition
  • Object becomes a property key
  • Compare "3" == 3
  • Compare "10" < "2"
  • Find Symbol.iterator
  • Call next and read done
Try it yourself
0 of 6 correct

Sort each situation by its primary rule, then read why it fits.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

A specification name is a tool for explanation. It does not become a callable JavaScript global, and it does not promise a specific internal function call in your engine. Ask which behavior is visible to your code.

Loose and strict equality differPop out in the code editor (opens in a new tab)JavaScript
const price = { valueOf() { return 3; } };
console.log(price == 3, price === 3);

Line 1 creates an object that supplies 3 when converted. Line 2 prints true false. Loose equality converts this object against a Number; strict equality keeps the type difference. Do not replace this rule with the slogan that == always converts everything.

  • "A hint guarantees the result type." It changes preferred method order; a hook can return another primitive type.
  • "Every == call converts both sides." Same-type values use strict comparison; object versus null does not call valueOf.
  • "Greater-than converts the right object first." leftFirst preserves the source's left-to-right effects.
  • "IteratorStep returns the value." It returns a result object or DONE; IteratorStepValue reads value.
  • "Species changes the input array." It influences certain newly created results only.

An iterator's normal exhaustion and an early break are also different endings. A missing return method is allowed, but a defined one may be called during early closing. Keep this distinction in mind when an iterable owns a resource such as an open stream.

Conversion rules compared
RuleOutputWhat starts it
ToPrimitiveObject to a non-object valueA preferred hint; a hook can return any primitive
ToPropertyKeyValue to a String or Symbol keyFirst uses ToPrimitive with a string hint
ToNumericValue to Number or BigIntFirst uses ToPrimitive with a number hint

Practice exercises

Predict before running the starter code. Each question asks for a real output rather than a spec label alone. Once you know the output, name the operation that explains it.

Exercise 1 · Warm-upPredict a price conversion

A receipt stores its price in an object with valueOf. Read the code and type the number printed. Which method supplied the first operand?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const price = { valueOf() { return 3; } };
console.log(price + 2);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the cart key

    The cart stores tea under a numeric-looking key. Predict the one word logged when code reads with a Number. What does ToPropertyKey produce?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cart = { "3": "tea" };
    console.log(cart[3]);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCompare without guessing

      A form supplies the text "3" while a price is stored as the Number 3. Predict both Boolean outputs. Would you use these two operators interchangeably in checkout code?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      console.log("3" == 3, "3" === 3);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeStop a shopping-list loop

        The app scans a shopping list and stops at the first item. What word is logged? Explain why the second item is never printed.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const items = ["tea", "milk"];
        for (const item of items) { console.log(item); break; }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeRead a mapped array's type

          Your app subclasses Array for orders, but map should return a plain array. The subclass provides a species getter. Does the mapped result inherit from Orders?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          class Orders extends Array { static get [Symbol.species]() { return Array; } }
          console.log(new Orders("tea").map(item => item) instanceof Orders);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply conversion to a real site

            A checkout form hands you a field object instead of a String. The cart displays the wrong item. Reduce the lookup to this example and predict what prints before debugging the full UI.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const cart = { tea: 3 };
            const key = { toString() { return "tea"; } };
            console.log(cart[key]);

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Each question has one answer. For code questions, run the small program and match its output exactly; for concept questions, say which named rule handles the operation before choosing.

              Abstract operations quiz · 7 questionsScore: first tries count
              1. Question 1 of 7When an object has no Symbol.toPrimitive hook, which method does OrdinaryToPrimitive try first with a number hint?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this computed key print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cart = { 3: "tea" };
                console.log(cart[3]);

                Choose an answer to see the explanation.

              3. Question 3 of 7What does ToNumeric return if ToPrimitive produces 3n?

                Choose an answer to see the explanation.

              4. Question 4 of 7What do these two comparisons print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log("10" < "2", "10" < 2);

                Choose an answer to see the explanation.

              5. Question 5 of 7Why does IsLessThan take leftFirst?

                Choose an answer to see the explanation.

              6. Question 6 of 7What does IteratorStep return before the value is read?

                Choose an answer to see the explanation.

              7. Question 7 of 7What does the mapped subclass result inherit from?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                class Orders extends Array { static get [Symbol.species]() { return Array; } }
                console.log(new Orders("tea").map(item => item) instanceof Orders);

                Choose an answer to see the explanation.

              If an answer surprised you, return to the nearest runnable example. Conversion order and iterator cleanup are easier to remember after watching the actual console output.

              Key takeaways

              Start at a JavaScript expression and trace only the required operations. The named rules describe language behavior you can test without claiming to inspect engine internals.

              • ToPrimitive consults Symbol.toPrimitive first, then ordinary methods in hint-dependent order.
              • ToPropertyKey returns a String or Symbol; ToNumeric returns a Number or BigInt.
              • IsLooselyEqual and IsLessThan have different conversion cases; leftFirst preserves visible comparison order.
              • GetIterator obtains the iterator; IteratorStep checks done; IteratorStepValue reads a value when available.
              • SpeciesConstructor explains how a suitable constructor can be selected for certain derived results.

              Remember the one-liner.
              An abstract operation is a named rule for required behavior, not a function your page can call.

              Coming next: Lexical grammar & ASI. It asks how source characters become tokens before any of these operations can run.

              CompleteFrontend Clear concepts. Working examples.