Well-known symbols
Hook into JavaScript language behavior with built-in symbols such as iterator, toPrimitive, toStringTag, hasInstance, species, string protocol hooks, and dispose.
- 01Name each hookMatch each well-known symbol to the built-in code that calls it.
- 02Trace real callsPredict the hint, the arguments, and the output when JavaScript calls a symbol method.
- 03Use them with careKnow which hooks are common, which are rare, and which you must check for before you use them.
Built-in keys that JavaScript looks for
A symbol is a special value you can use as a property key. Every symbol is unique, so it never clashes with a normal key like "name". You met symbols in the Symbols lesson.
JavaScript also comes with a few symbols that are already made. They live on Symbol, for example Symbol.iterator. These are called well-known symbols.
Here is why they matter. Some built-in code, like spread [...list] or a for...of loop, looks for a method at one of these keys. If your object has that method, JavaScript calls it for you. In this lesson, a method like that is called a hook.
Think of a TV. The company that made it decided what the HDMI socket and the USB socket are for. You don't build a new socket. You plug in the cable that fits, and the TV does its job.
- In real life: The HDMI, USB, and power sockets are already on the TV
- In JavaScript: JavaScript already knows
Symbol.iterator,Symbol.toPrimitive, and the others - In real life: You plug in the matching cable
- In JavaScript: Your object has a method at the matching symbol key
- In real life: The TV uses that socket for one known job
- In JavaScript: A built-in operation calls that method
- In real life: A label beside each socket tells you its job
- In JavaScript: The symbol's name tells you which operation uses it
The table lists every hook in this lesson. Each row is one well-known symbol: the code that calls it, and when to use it.
| Symbol | Which code calls it? | When should you use it? |
|---|---|---|
Symbol.iterator | for...of, spread, destructuring, and Array.from call it. | Use it for your own lists of things. |
Symbol.asyncIterator | for await...of calls it. | Use it for values that arrive later, like pages from a server. |
Symbol.toPrimitive | Template literals, math, +, and == call it. | Use it only when the result is obvious. A plain method is often clearer. |
Symbol.toStringTag | Object.prototype.toString.call(value) reads it. | Use it to give an object a clearer label in logs. |
Symbol.hasInstance | value instanceof Right calls it on Right. | Rarely. A clear check function is easier to read. |
Symbol.species | Some array methods, like map, read it when they make a new array. | Avoid it in new code. It makes code harder to check for safety and slower to run. |
| String method symbols | match, matchAll, replace, search, and split call them. | Use them for small objects that act like a regex. |
Symbol.dispose | using, DisposableStack, or your own finally block calls it. | It is new. Check that the tools exist before you use them. |
A hook lets your object work with built-in code, like an array does. Use hooks with care, though. A good hook gives the result a reader expects. A bad hook makes normal-looking code do something surprising.
Symbol.iterator and Symbol.asyncIterator
INTERACTIVEThe loop hooks are the easiest place to start. These four all call Symbol.iterator to get values from an object:
- a
for...ofloop - spread, like
[...cart] - destructuring, like
const [first] = cart Array.from(cart)
Symbol.asyncIterator does the same job for a for await...of loop. That loop waits for each value, because each one may arrive later.
The Iteration protocols lesson shows every detail of how this works. Here, just follow three steps: the loop finds the method at the symbol key, calls it, and takes the values it hands back.
const cart = {
*[Symbol.iterator]() { yield "tea"; yield "bread"; },
};
console.log([...cart].join(", "));Line 2 adds a method to cart. The square brackets around Symbol.iterator let you use the symbol as the key. The star * makes the method a generator: a function that hands out values one at a time with yield. This one yields "tea", then "bread".
Line 4 spreads the cart with [...cart]. JavaScript calls the method, collects both values, and prints tea, bread. Without the method, spread throws a TypeError. It doesn't guess how to read the object.
cart for the method at the key Symbol.iterator and calls it. The method hands back "tea", then "bread".(async () => {
const pages = { async *[Symbol.asyncIterator]() { yield 1; yield 2; } };
const values = [];
for await (const page of pages) values.push(page);
console.log(values.join(", "));
})();Line 2 gives pages an async generator at the key Symbol.asyncIterator. It yields the page numbers 1 and 2. Line 4 is a for await...of loop. It waits for each page and pushes it into values. Line 5 prints 1, 2. A normal for...of loop can't use this method; it only looks for Symbol.iterator.
const gadget = { name: "price tag", value: 25, [Symbol.toPrimitive](hint) { return hint === "string" ? "PriceTag" : 25; }, [Symbol.toStringTag]: "CatalogPrice", [Symbol.iterator]: function* () { yield "label"; yield "value"; }, async *[Symbol.asyncIterator]() { yield "async label"; yield "async value"; }, [Symbol.isConcatSpreadable]: true,}; class PriceLike { static [Symbol.hasInstance](value) { return value?.value === 25; }}Turn hooks on or off, then press an operation.Press an operation to run normal JavaScript on gadget. The log shows which symbol method answered. Turn a hook off to see what happens without it: the normal result, or an error.
Try this: turn Symbol.iterator off, then press spread. You don't get an empty array. You get an error, because spread needs that method. Then turn Symbol.asyncIterator off and press for await to see the same thing for the async loop.
Symbol.hasInstance and Symbol.species
RARE HOOKSvalue instanceof Thing asks: "Was value made by Thing?" Normally it checks whether Thing.prototype is in the value's prototype chain. The prototype chain is the list of objects JavaScript searches when it looks up a property.
But first, instanceof checks the right side for a method at Symbol.hasInstance. If it finds one, it calls Thing[Symbol.hasInstance](value) and uses that answer instead.
class Adult {
static [Symbol.hasInstance](user) { return user.age >= 18; }
}
console.log({ age: 20 } instanceof Adult);Line 2 is a static method, so it belongs to the class Adult itself. It returns true when the age is 18 or more. Line 4 tests a plain object, { age: 20 }. That object was never made by Adult. Still, instanceof calls the method, and it prints true.
Symbol.species answers a different question. Some array methods, like map(), make a new array. If you call them on a subclass of Array, what kind of object should they make? A subclass is a class made with extends. A subclass can use Symbol.species to say: "Make a plain Array instead."
This hook exists so older code keeps working. New code should avoid it. It makes code harder to check for safety, and harder for JavaScript engines to speed up.
class Notes extends Array {
static get [Symbol.species]() { return Array; }
}
const notes = new Notes("read", "write");
console.log(notes.map((note) => note.toUpperCase()) instanceof Notes);Line 1 makes Notes, a subclass of Array. Line 2 is a static get: a getter on the class, which runs when the key is read. It returns Array. On line 5, map reads this key to choose what to make. So the new array is a plain Array, and line 5 prints false.
class Bag extends Array { static get [Symbol.species]() { return Array; }} const bag = new Bag("notebook", "pencil");const result = bag.map((item) => item.toUpperCase());console.log(result instanceof Bag);console.log(result instanceof Array);falsetrue["NOTEBOOK", "PENCIL"]Bag is a subclass of Array. Its Symbol.species getter returns Array, so map() makes a plain array, not a Bag.
A clear function like isPriceLike(value) is easier to read than a surprising instanceof hook. Use these two symbols only when you build a library that must act like built-in objects. Don't use them for everyday app data.
Symbol.isConcatSpreadable and Symbol.unscopables
EDGE CASES[].concat(value) joins values into a new array. It has its own rule:
- An array is opened up: each of its items is added one by one.
- A plain object is added as one item.
Symbol.isConcatSpreadable changes that rule for one value. Set it to true on an array-like object, and concat opens it up. An array-like object has number keys, like 0 and 1, and a length. Set it to false on an array, and concat adds the whole array as one item.
const row = {
0: "tea", 1: "cake", length: 2,
[Symbol.isConcatSpreadable]: true,
};
console.log(["menu"].concat(row).join(" / "));Line 2 gives row the keys 0 and 1 and a length of 2. Line 3 sets the flag to true. So on line 5, concat adds tea and cake one by one, and it prints menu / tea / cake. Without the flag, it would add the whole object as one item.
Symbol.unscopables is mostly history. Very old code used a with statement, which lets you write includes instead of list.includes. Strict mode does not allow with, and this site never runs it.
When new array methods like includes were added, old with code could break. So arrays keep a list of names that with must skip. That list is at Array.prototype[Symbol.unscopables].
const hiddenFromWith = Array.prototype[Symbol.unscopables];console.log(hiddenFromWith.includes);console.log(hiddenFromWith.find);console.log(hiddenFromWith.at);truetruetrueat, copyWithin, entries, fill, find, findIndex, findLast, findLastIndexSymbol.unscopables is a list of array method names that old with statements must skip. This page shows the list. It never runs with.
with, and new code should not use it. The list keeps old code working after new array methods were added.const hidden = Array.prototype[Symbol.unscopables];
console.log(hidden.includes);
console.log(hidden.at);Line 1 reads the list from the array prototype. Lines 2 and 3 check two names, and both print true: old with code skips includes and at. Don't write with in new code. You only need this symbol when you read old code.
String method hooks: match, matchAll, replace, search, split
INTERACTIVEA regex (regular expression) is a pattern for finding text, like /cat/. You usually pass one to string methods such as replace. But you can pass your own object instead. Each of these string methods looks for a matching symbol method on the object you pass in:
matchcallsSymbol.match.matchAllcallsSymbol.matchAll.replacecallsSymbol.replace.searchcallsSymbol.search.splitcallsSymbol.split.
If the method is there, the string hands the work to it. In this lesson, an object like that is called a matcher.
const matcher = { [Symbol.match](text) { return text.includes("cat") ? "found" : "missing"; } };
console.log("cat nap".match(matcher));Line 1 makes a matcher with a Symbol.match method. It returns "found" if the text has cat in it. Line 2 calls "cat nap".match(matcher). The string calls the method, so it prints found. The other four methods work the same way. Each example below uses one.
const matcher = { flags: "g", [Symbol.matchAll](text) { return text.matchAll(/cat/g); } };
console.log([..."cat cat".matchAll(matcher)].length);The matcher's method finds every cat with the real regex /cat/g. The g means "global": find every match, not just the first. The matcher also needs flags: "g", because matchAll checks for it first. Line 2 spreads the two matches into an array and prints its length, 2.
const matcher = { [Symbol.replace](text, replacement) { return text.replaceAll("cat", replacement); } };
console.log("cat nap".replace(matcher, "dog"));The method gets two arguments: the whole text, "cat nap", and the new word, "dog". It swaps every cat for dog, so line 2 prints dog nap.
Symbol.replace method with the text and "dog", then prints what the method returns: dog nap.const matcher = { [Symbol.search](text) { return text.indexOf("cat"); } };
console.log("scatter".search(matcher));The method returns the position where cat starts in "scatter". Positions start at 0, so s is 0 and c is 1. Line 2 prints 1. Each hook works on its own, so you only write the ones you need.
const matcher = { [Symbol.split](text) { return text.split("/"); } };
console.log("tea/cake".split(matcher).join(" + "));The method cuts the text at each /, so "tea/cake" becomes ["tea", "cake"]. Line 2 joins the pieces and prints tea + cake. This method runs only for split, never for search or replace.
const matcher = { needle: "cat", flags: "g", [Symbol.match](text) { return text.includes(this.needle); }, [Symbol.matchAll](text) { return text.matchAll(/cat/g); }, [Symbol.replace](text, replacement) { return text.replaceAll(this.needle, replacement); }, [Symbol.search](text) { return text.indexOf(this.needle); }, [Symbol.split](text) { return text.split(this.needle); },}; "cat nap cat".replace(matcher, "dog");Running "cat nap cat".replace(matcher) hands the work to the matcher's matching symbol method. Result: dog nap dog.
If an object has a Symbol.match key with a truthy value, JavaScript treats the object like a regex. That has two effects:
startsWithandincludesthrow aTypeErrorif you pass it in, as they do for a real regex.matchAllreads the object'sflags. Without ag, it throws before yourSymbol.matchAllmethod runs. That's why the lab's matcher hasflags: "g".
Symbol.dispose and Symbol.asyncDispose
FEATURE DETECTSome things must be cleaned up when you finish with them, even if your code fails halfway. Examples are an open file, a lock, or a subscription. JavaScript gives that cleanup method a standard key:
Symbol.disposefor cleanup that finishes right away.Symbol.asyncDisposefor cleanup you must wait for.
You rent a bike for a trip. The form says you must return it. If the trip goes wrong, you still bring the bike back.
- In real life: The rental form says: return the bike when you're done
- In JavaScript: A resource object has a
[Symbol.dispose]()method - In real life: The trip goes wrong halfway
- In JavaScript: The code in
trythrows an error - In real life: You still return the bike
- In JavaScript:
finally,using, or a stack still calls the cleanup method
const rental = { open: true, [Symbol.dispose]() { this.open = false; console.log("returned rental"); },}; try { console.log("using rental"); throw new Error("trip failed");} finally { rental[Symbol.dispose]();}console.log(rental.open);checkingcheckingcheckingcheckingusing rental | caught trip failed | returned rental | falseThe code uses try/finally, which works in every browser. The checks on the right test your browser: does it have the newer stack tools, and can it read the using keyword?
using in its own code.In the lab's code, line 3 is the cleanup method. It sets open to false and prints returned rental. Line 10 prints using rental, then line 11 throws an error. A finally block always runs, even after an error, so line 13 calls the cleanup method. Then the error goes on, so line 15 never runs.
finally calls the cleanup method before the error leaves, so returned rental prints second. Then the error goes on, so line 15 never runs.JavaScript also has newer tools that call the cleanup method for you:
- the
usingandawait usingkeywords DisposableStackandAsyncDisposableStack, which collect several cleanups
At the time of writing, these are new. Some browsers and servers have the symbols but not these tools. So check that a tool exists before you use it. This is called feature detection. In a library, keep a try/finally version too, because it works everywhere.
Where you will use this
You will read these hooks more often than you write them. They show up in custom lists, form-checking libraries, price and date objects, data that arrives in pages, test helpers, and cleanup code. Before you write one, ask: "Do I want built-in code, like spread or a template literal, to use this object?"
- Use
Symbol.iteratorwhen the value is a list of things. - Use
Symbol.asyncIteratorwhen the values arrive later. - Use the string method hooks for small matcher objects.
- Use
Symbol.toPrimitiveonly when the result is obvious, and write down what it does. - Use the dispose hooks for cleanup, with feature detection and a
try/finallyversion.
class Cart {
constructor(items) { this.items = items; }
get [Symbol.toStringTag]() { return "Cart"; }
*[Symbol.iterator]() { yield* this.items; }
[Symbol.toPrimitive](hint) { return hint === "number" ? this.items.length : `Cart(${this.items.length})`; }
}
const cart = new Cart(["tea", "bread"]);
console.log([...cart].join(", "));
console.log(Object.prototype.toString.call(cart));
console.log(+cart);Cart is a normal class that keeps its things in items. It has three hooks:
- Line 3 is a getter for the tag, so line 9 prints
[object Cart]. - Line 4 is the loop hook, so the spread on line 8 prints
tea, bread. - Line 5 is the primitive hook. The
+in front ofcarton line 10 sends the"number"hint, so it prints2, the number of items.
Each hook serves one built-in operation. The class still keeps its data in a plain items property that you can read directly.
${price}asks the object for a primitiveObject.prototype.toString.call(value)says[object Module][...range]gets values from your objectfor await...of feedgets pages that arrive later[].concat(box)opens the box up or adds it wholesubclass.map(...)chooses what kind of array to maketext.replace(matcher, "x")hands the work to the matchertext.matchAll(matcher)hands the work to the matcher
Put each line of code in the group of symbols it uses.
Common misconceptions
"Symbol properties are private."
Symbol keys never clash with other keys. But any code can still find them with Object.getOwnPropertySymbols or Reflect.ownKeys.
"One hook changes every way an object is shown."
Symbol.toPrimitive, toString(), and Symbol.toStringTag each change a different thing.
"instanceof always checks the prototype chain."
Usually it does. But if the right side has a Symbol.hasInstance method, that method decides instead.
"Species is the modern way to build list classes."
It exists so older code keeps working. In new code, write plain methods that say what they return.
"Dispose runs by itself when an object is no longer used."
No. Something must call it: a finally block, a stack, or using.
Each row of this table shows something that is easy to get wrong, and what is true instead.
| What people think | What is true |
|---|---|
| A symbol hook is a hidden event listener. | It is just a property with a symbol key. JavaScript calls it only during one kind of operation. |
toStringTag changes what a template literal prints. | It changes only Object.prototype.toString.call(value). It does not change ${value}. |
instanceof always checks the prototype chain. | Usually it does. But the right side can have a Symbol.hasInstance method that decides instead. |
species is a good habit for subclasses. | It exists so older code keeps working. It makes code harder to check for safety and slower to run. |
Practice: match the hook to the code
4 EXERCISESRead the code. Type the four lines it prints, in order.
const badge = {
[Symbol.toPrimitive](hint) {
console.log(hint);
return hint === "number" ? 7 : "badge";
},
};
console.log(badge * 3);
console.log(`${badge}`);The lines are number, 21, string, badge. Line 7 multiplies, so the hook logs number and returns 7, and 7 * 3 prints 21. Line 8 is a template literal, so the hook logs string and returns badge.
The code prints 1, but you want 2. Which symbol makes concat add the items of row one by one?
const row = { 0: "Ada", 1: "Lin", length: 2 };
console.log([].concat(row).length);const row = { 0: "Ada", 1: "Lin", length: 2, [Symbol.isConcatSpreadable]: true };
console.log([].concat(row).length); // 2Symbol.isConcatSpreadable set to true tells concat to add the items one by one, instead of adding the whole object as one item. So the length is 2.
Predict the text that the matcher code prints.
const onlyCats = {
[Symbol.replace](text, replacement) {
return text.replaceAll("cat", replacement);
},
};
console.log("cat nap cat".replace(onlyCats, "dog"));The matcher's Symbol.replace method gets the text cat nap cat and the word dog. It swaps every cat for dog, so it prints dog nap dog.
For a shopping cart, would you add Symbol.toPrimitive so that cart + 0 gives the total? Or would you write a cart.totalCents() method? Explain your choice.
// Usually clearer:
cart.totalCents();
// A hook is good when built-in code is the point:
[...playlist];Use a hook when built-in code really should use your object, like spreading a playlist. For app actions like a total, use a plain method name, so readers don't have to guess what + does.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8Which code calls
Symbol.iterator?Choose an answer to see the explanation.
Question 2 of 8Which hints does this code log, and what does it print?
Read the code, then predictconst item = { [Symbol.toPrimitive](hint) { console.log(hint); return hint === "string" ? "box" : 4; }, }; console.log(`${item}`); console.log(item + "");Choose an answer to see the explanation.
Question 3 of 8What does this tagged object print?
Read the code, then predictconst file = { [Symbol.toStringTag]: "Upload" }; console.log(Object.prototype.toString.call(file));Choose an answer to see the explanation.
Question 4 of 8In
value instanceof Thing, which side gives theSymbol.hasInstancemethod?Choose an answer to see the explanation.
Question 5 of 8What does
concatprint with this flag?Read the code, then predictconst pair = { 0: "a", 1: "b", length: 2, [Symbol.isConcatSpreadable]: true }; console.log(["start"].concat(pair).join("-"));Choose an answer to see the explanation.
Question 6 of 8Why is
Symbol.speciesrare in new code?Choose an answer to see the explanation.
Question 7 of 8What does this matcher make
searchprint?Read the code, then predictconst matcher = { [Symbol.search](text) { return text.indexOf("cat"); }, }; console.log("scatter".search(matcher));Choose an answer to see the explanation.
Question 8 of 8Which statement about
Symbol.disposeandusingis true?Choose an answer to see the explanation.
Key takeaways
- Well-known symbols are built-in keys. Built-in code looks for methods at these keys and calls them.
Symbol.iteratorletsfor...ofand spread read an object.Symbol.asyncIteratordoes the same forfor await...of.Symbol.toPrimitivegets the hintstring,number, ordefault.Symbol.toStringTagsets the word in[object Tag].Symbol.hasInstance,Symbol.species,Symbol.isConcatSpreadable, andSymbol.unscopableseach change one small behavior. You rarely need them.- The string method hooks and the dispose hooks are useful. Check that dispose tools exist, and keep hooks unsurprising.
Remember the one-liner.
A well-known symbol is a built-in key that lets your object work with one specific piece of built-in JavaScript code.
Up next: Decorators, reusable wrappers for classes, methods, and fields.