In ECMAScript 6, the object Symbol
has several properties that contain so-called well-known symbols (Symbol.iterator
, Symbol.hasInstance
, etc.). These let you customize how ES6 treats objects. This blog post explains the details.
Implementation of the features described here is work in progress. Consult the “ECMAScript 6 compatibility table” for what is supported where (spoiler: not much, in few engines).
This section covers knowledge that is useful for the remainder of this post. Additionally, the following material may be of interest:
The ES6 specification uses internal properties to describe how JavaScript works. These are only known to the spec and not accessible from JavaScript. They may or may not exist in an actual implementation of the language. The names of internal properties are written in double square brackets.
For example: the link between an object and its prototype is the internal property [[Prototype]]
. The value of that property cannot be read directly via JavaScript, but you can use Object.getPrototypeOf()
to do so.
If an object obj
inherits a property prop
that is read-only then you can’t assign to that property:
let proto = Object.defineProperty({}, 'prop', {
writable: false,
configurable: true,
value: 123,
});
let obj = Object.create(proto);
obj.prop = 456;
// TypeError: Cannot assign to read-only property
This is similar to how an inherited property works that has a getter, but no setter. It is in line with viewing assignment as changing the value of an inherited property. It does so non-destructively: the original is not modified, but overridden by a newly created own property. Therefore, an inherited read-only property and an inherited setter-less property both prevent changes via assignment. You can, however, force the creation of an own property via Object.defineProperty()
:
let proto = Object.defineProperty({}, 'prop', {
writable: false,
configurable: true,
value: 123,
});
let obj = Object.create(proto);
Object.defineProperty(obj, 'prop', {value: 456});
console.log(obj.prop); // 456
All well-known symbols in ES6 are keys for properties. If you add a property to an object that has one of those keys, you change how ES6 treats that object. These are all well-known symbols in ES6:
Symbol.hasInstance
(method)instanceof
.Symbol.toPrimitive
(method)Symbol.toStringTag
(string)Object.prototype.toString()
.Symbol.iterator
(method)for-of
loop and the spread operator (...
)). Details: chapter “Iterables and iterators” of “Exploring ES6”.Symbol.match
Symbol.replace
Symbol.search
Symbol.split
Symbol.unscopables
(Object)with
statement.Symbol.species
(method)this
.Symbol.isConcatSpreadable
(boolean)Array.prototype.concat()
adds the indexed elements of an object to its result (“spreading”) or the object as a single element.The following sections have more information on categories 1, 3 and 4.
Symbol.hasInstance
(method) A method with the key Symbol.hasInstance
lets an object C
customize the behavior of the instanceof
operator. Signature of that method:
[Symbol.hasInstance](potentialInstance : any)
x instanceof C
works as follows in ES6:
C
is not an object, throw a TypeError
.C[Symbol.hasInstance](x)
, coerce the result to boolean and return it.C
must be callable, C.prototype
in the prototype chain of x
, etc.).The only method in the standard library that has this key is:
Function.prototype[Symbol.hasInstance]()
This is the implementation of instanceof
that all functions (including classes) use by default. Quoting the spec:
This property is non-writable and non-configurable to prevent tampering that could be used to globally expose the target function of a bound function.
The tampering is possible because the traditional instanceof
algorithm, OrdinaryHasInstance()
, applies instanceof
to the target function if it encounters a bound function.
Given that this property is read-only, you can’t use assignment to override it, as mentioned earlier.
As an example, let’s implement an object ReferenceType
whose “instances” are all objects, not just objects that are instances of Object
(and therefore have Object.prototype
in their prototype chains).
const ReferenceType = {
[Symbol.hasInstance](value) {
return (value !== null
&& (typeof value === 'object'
|| typeof value === 'function'));
}
};
const obj1 = {};
console.log(obj1 instanceof Object); // true
console.log(obj1 instanceof ReferenceType); // true
const obj2 = Object.create(null);
console.log(obj2 instanceof Object); // false
console.log(obj2 instanceof ReferenceType); // true
Symbol.toPrimitive
(method) Symbol.toPrimitive
lets an object customize how it is coerced (converted automatically) to a primitive value.
Many JavaScript operations coerce values to the types that they need.
*
) coerces its operands to numbers.new Date(year, month, date)
coerces its parameters to numbers.parseInt(string , radix)
coerces its first parameter to a string.The following are the most common coercions:
true
for truthy values, false
for falsy values. Objects are always truthy (even new Boolean(false)
).null
→ 0
, true
→ 1
, '123'
→ 123
, etc.).null
→ 'null'
, true
→ 'true'
, 123
→ '123'
, etc.).b
via new Boolean(b)
, numbers n
via new Number(n)
, etc.).Converting an arbitrary value to a primitive is handled via the spec-internal operation ToPrimitive()
which has three modes:
The default mode is only used by:
==
)+
)new Date(value)
(exactly one parameter!)If the value is a primitive then ToPrimitive()
is already done. Otherwise, the value is an object obj
, which is converted to a promitive as follows:
obj.valueOf()
if it is primitive. Otherwise, return the result of obj.toString()
if it is primitive. Otherwise, throw a TypeError
.toString()
is called first, valueOf()
second.This normal algorithm can be overridden by giving an object a method with the following signature:
[Symbol.toPrimitive](hint : 'default' | 'string' | 'number')
In the standard library, there are two such methods:
Symbol.prototype[Symbol.toPrimitive](hint)
prevents toString()
from being called (which throws an exception).
Date.prototype[Symbol.toPrimitive](hint)
This method implements behavior that deviates from the default algorithm. Quoting the specification: “Date objects are unique among built-in ECMAScript object in that they treat 'default'
as being equivalent to 'string'
. All other built-in ECMAScript objects treat 'default'
as being equivalent to 'number'
.”
The following code demonstrates how coercion affects the object obj
.
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
console.log(2 * obj); // 246
console.log(3 + obj); // '3default'
console.log(obj == 'default'); // true
console.log(String(obj)); // 'str'
Symbol.toStringTag
(string) In ES5 and earlier, each object had the internal own property [[Class]]
whose value hinted at its type. You could not access it directly, but its value was part of the string returned by Object.prototype.toString()
, which is why that method was used for type checks, as an alternative to typeof
.
In ES6, there is no internal property [[Class]]
, anymore, and using Object.prototype.toString()
for type checks is discouraged. In order to ensure the backwards-compatibility of that method, the public property with the key Symbol.toStringTag
was introduced. You could say that it replaces [[Class]]
.
Object.prototype.toString()
now works as follows:
this
to an object obj
.tst
of obj
.'[object ' + tst + ']'
.The default values for various kinds of objects are shown in the following table.
Value | toString tag |
---|---|
undefined |
'Undefined' |
null |
'Null' |
An Array object | 'Array' |
A string object | 'String' |
arguments |
'Arguments' |
Something callable | 'Function' |
An error object | 'Error' |
A boolean object | 'Boolean' |
A number object | 'Number' |
A date object | 'Date' |
A regular expression object | 'RegExp' |
(Otherwise) | 'Object' |
Most of the checks in the left column are performed by looking at internal properties. For example, if an object has the internal property [[Call]]
, it is callable.
The following interaction demonstrates the default toString tags.
> Object.prototype.toString.call(null)
'[object Null]'
> Object.prototype.toString.call([])
'[object Array]'
> Object.prototype.toString.call({})
'[object Object]'
> Object.prototype.toString.call(Object.create(null))
'[object Object]'
If an object has an (own or inherited) property whose key is Symbol.toStringTag
then its value overrides the default toString tag. For example:
> ({}.toString())
'[object Object]'
> ({[Symbol.toStringTag]: 'Foo'}.toString())
'[object Foo]'
Instances of user-defined classes get the default toString tag (of objects):
class Foo { }
console.log(new Foo().toString()); // [object Object]
One option for overriding the default is via a getter:
class Bar {
get [Symbol.toStringTag]() {
return 'Bar';
}
}
console.log(new Bar().toString()); // [object Bar]
In the JavaScript standard library, there are the following custom toString tags. Objects that have no global names are quoted with percent symbols (for example: %TypedArray%
).
JSON[Symbol.toStringTag]
→ 'JSON'
Math[Symbol.toStringTag]
→ 'Math'
M
: M[Symbol.toStringTag]
→ 'Module'
ArrayBuffer.prototype[Symbol.toStringTag]
→ 'ArrayBuffer'
DataView.prototype[Symbol.toStringTag]
→ 'DataView'
Map.prototype[Symbol.toStringTag]
→ 'Map'
Promise.prototype[Symbol.toStringTag]
→ 'Promise'
Set.prototype[Symbol.toStringTag]
→ 'Set'
get %TypedArray%.prototype[Symbol.toStringTag]
→ 'Uint8Array'
etc.WeakMap.prototype[Symbol.toStringTag]
→ 'WeakMap'
WeakSet.prototype[Symbol.toStringTag]
→ 'WeakSet'
%MapIteratorPrototype%[Symbol.toStringTag]
→ 'Map Iterator'
%SetIteratorPrototype%[Symbol.toStringTag]
→ 'Set Iterator'
%StringIteratorPrototype%[Symbol.toStringTag]
→ 'String Iterator'
Symbol.prototype[Symbol.toStringTag]
→ 'Symbol'
Generator.prototype[Symbol.toStringTag]
→ 'Generator'
GeneratorFunction.prototype[Symbol.toStringTag]
→ 'GeneratorFunction'
All of the built-in properties whose keys are Symbol.toStringTag
have the following property descriptor:
{
writable: false,
enumerable: false,
configurable: true,
}
As mentioned in an earlier section, you can’t use assignment to override those properties, because they are read-only.
In ES6, the four string methods that accept regular expression parameters do relatively little. They mainly call methods of their parameters:
String.prototype.match(regexp)
calls regexp[Symbol.match](this)
.String.prototype.replace(searchValue, replaceValue)
calls searchValue[Symbol.replace](this, replaceValue)
.String.prototype.search(regexp)
calls regexp[Symbol.search](this)
.String.prototype.split(separator, limit)
calls separator[Symbol.split](this, limit)
.The parameters don’t have to be regular expressions, anymore. Any objects with appropriate methods will do.
Symbol.unscopables
(Object) Symbol.unscopables
lets an object hide some properties from the with
statement.
The reason for doing so is that it allows TC39 to add new methods to Array.prototype
without breaking old code. Note that current code rarely uses with
, which is forbidden in strict mode and therefore ES6 modules (which are implicitly in strict mode).
Why would adding methods to Array.prototype
break code that uses with
(such as the widely deployed Ext JS 4.2.1)? Take a look at the following code. The existence of a property Array.prototype.values
breaks foo()
, if you call it with an Array:
function foo(values) {
with (values) {
console.log(values.length); // abc (*)
}
}
Array.prototype.values = { length: 'abc' };
foo([]);
Inside the with
statement, all properties of values
become local variables, shadowing even values
itself. Therefore, if values
has a property values
then the statement in line * logs values.values.length
and not values.length
.
Symbol.unscopables
is used only once in the standard library:
Array.prototype[Symbol.unscopables]
with
statement): copyWithin
, entries
, fill
, find
, findIndex
, keys
, values
Symbol.species
(method) Symbol.species
lets you configure how methods of built-in objects create instances they return. One example is that you can configure what Array.prototype.map()
returns. By default, it uses the same constructor that created this
to create the return value, but you can override that by setting Array[Symbol.species]
.
The details are explained in the chapter on classes of “Exploring ES6”.
Symbol.isConcatSpreadable
(boolean) Symbol.isConcatSpreadable
lets you configure how Array.prototype.concat()
adds an object to its result.
The default for Arrays is to “spread” them, their indexed elements become elements of the result:
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e'); // ['a', 'b', 'c', 'd', 'e']
With Symbol.isConcatSpreadable
, you can override the default and avoid spreading for Arrays:
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e'); // ['a', 'b', ['c','d'], 'e']
For non-Arrays, the default is not to spread. You can use Symbol.isConcatSpreadable
to force spreading:
let obj = {length: 2, 0: 'c', 1: 'd'};
console.log(['a', 'b'].concat(obj, 'e')); // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
console.log(['a', 'b'].concat(obj, 'e')); // ['a', 'b', 'c', 'd', 'e']
The default in ES6 is to spread only Array objects. Whether or not something is an Array object is tested via Array.isArray()
(or rather, the same operation that that method uses). Whether or not Array.prototype
is in the prototype chain makes no difference for that test (which is important, because, in ES5 and earlier, hacks were used to subclass Array
and those must continue to work; see blog post “__proto__
in ECMAScript 6”):
> let arr = [];
> Array.isArray(arr)
true
> Object.setPrototypeOf(arr, null);
> Array.isArray(arr)
true
The default can be overridden by adding a property whose key is Symbol.isConcatSpreadable
to the object itself or to one of its prototypes, and by setting it to either true
or false
.
No object in the ES6 standard library has a property with the key Symbol.isConcatSpreadable
. This mechanism therefore exists purely for browser APIs and user code.
Consequences:
Subclasses of Array
are spread by default (because their instances are Array objects).
A subclass of Array
can prevent its instances from being spread by setting a property to false
whose key is Symbol.isConcatSpreadable
. That property can be a prototype property or an instance property.
Other Array-like objects are spread by concat()
if property [Symbol.isConcatSpreadable]
is true
. That would enable one, for example, to turn on spreading for some Array-like DOM collections.
Typed Arrays are not spread. They don’t have a method concat()
, either.
Symbol.isConcatSpreadable
in the ES6 spec Array.prototype.concat()
, you can see that spreading requires an object to be Array-like (property length
plus indexed elements).IsConcatSpreadable()
. The last step is the default (equivalent to Array.isArray()
) and the property [Symbol.isConcatSpreadable]
is retrieved via a normal Get()
operation, meaning that it doesn’t matter whether it is own or inherited.Symbol.hasInstance
and not Symbol.HAS_INSTANCE
(etc.)? The well-known symbols are stored in properties whose names start with lowercase characters and are camel-cased. In a way, these properties are constants and it is customary for constants to have all-caps names (Math.PI
etc.). But the reasoning for their spelling is different: Well-known symbols are used instead of normal property keys, which is why their “names” follow the rules for property keys, not the rules for constants.