Well Known Symbols
In the “Symbols” section of Chapter 2, we covered the new ES6 primitive type symbol
. In addition to symbols you can define in your own program, JS predefines a number of built-in symbols, referred to as Well Known Symbols (WKS).
These symbol values are defined primarily to expose special meta properties that are being exposed to your JS programs to give you more control over JS’s behavior.
We’ll briefly introduce each and discuss their purpose.
Symbol.iterator
In Chapters 2 and 3, we introduced and used the @@iterator
symbol, automatically used by ...
spreads and for..of
loops. We also saw @@iterator
as defined on the new ES6 collections as defined in Chapter 5.
Symbol.iterator
represents the special location (property) on any object where the language mechanisms automatically look to find a method that will construct an iterator instance for consuming that object’s values. Many objects come with a default one defined.
However, we can define our own iterator logic for any object value by setting the Symbol.iterator
property, even if that’s overriding the default iterator. The meta programming aspect is that we are defining behavior which other parts of JS (namely, operators and looping constructs) use when processing an object value we define.
Consider:
var arr = [4,5,6,7,8,9];
for (var v of arr) {
console.log( v );
}
// 4 5 6 7 8 9
// define iterator that only produces values
// from odd indexes
arr[Symbol.iterator] = function*() {
var idx = 1;
do {
yield this[idx];
} while ((idx += 2) < this.length);
};
for (var v of arr) {
console.log( v );
}
// 5 7 9
Symbol.toStringTag
and Symbol.hasInstance
One of the most common meta programming tasks is to introspect on a value to find out what kind it is, usually to decide what operations are appropriate to perform on it. With objects, the two most common inspection techniques are toString()
and instanceof
.
Consider:
function Foo() {}
var a = new Foo();
a.toString(); // [object Object]
a instanceof Foo; // true
As of ES6, you can control the behavior of these operations:
function Foo(greeting) {
this.greeting = greeting;
}
Foo.prototype[Symbol.toStringTag] = "Foo";
Object.defineProperty( Foo, Symbol.hasInstance, {
value: function(inst) {
return inst.greeting == "hello";
}
} );
var a = new Foo( "hello" ),
b = new Foo( "world" );
b[Symbol.toStringTag] = "cool";
a.toString(); // [object Foo]
String( b ); // [object cool]
a instanceof Foo; // true
b instanceof Foo; // false
The @@toStringTag
symbol on the prototype (or instance itself) specifies a string value to use in the [object ___]
stringification.
The @@hasInstance
symbol is a method on the constructor function which receives the instance object value and lets you decide by returning true
or false
if the value should be considered an instance or not.
Note: To set @@hasInstance
on a function, you must use Object.defineProperty(..)
, as the default one on Function.prototype
is writable: false
. See the this & Object Prototypes title of this series for more information.
Symbol.species
In “Classes” in Chapter 3, we introduced the @@species
symbol, which controls which constructor is used by built-in methods of a class that needs to spawn new instances.
The most common example is when subclassing Array
and wanting to define which constructor (Array(..)
or your subclass) inherited methods like slice(..)
should use. By default, slice(..)
called on an instance of a subclass of Array
would produce a new instance of that subclass, which is frankly what you’ll likely often want.
However, you can meta program by overriding a class’s default @@species
definition:
class Cool {
// defer `@@species` to derived constructor
static get [Symbol.species]() { return this; }
again() {
return new this.constructor[Symbol.species]();
}
}
class Fun extends Cool {}
class Awesome extends Cool {
// force `@@species` to be parent constructor
static get [Symbol.species]() { return Cool; }
}
var a = new Fun(),
b = new Awesome(),
c = a.again(),
d = b.again();
c instanceof Fun; // true
d instanceof Awesome; // false
d instanceof Cool; // true
The Symbol.species
setting defaults on the built-in native constructors to the return this
behavior as illustrated in the previous snippet in the Cool
definition. It has no default on user classes, but as shown that behavior is easy to emulate.
If you need to define methods that generate new instances, use the meta programming of the new this.constructor[Symbol.species](..)
pattern instead of the hard-wiring of new this.constructor(..)
or new XYZ(..)
. Derived classes will then be able to customize Symbol.species
to control which constructor vends those instances.
Symbol.toPrimitive
In the Types & Grammar title of this series, we discussed the ToPrimitive
abstract coercion operation, which is used when an object must be coerced to a primitive value for some operation (such as ==
comparison or +
addition). Prior to ES6, there was no way to control this behavior.
As of ES6, the @@toPrimitive
symbol as a property on any object value can customize that ToPrimitive
coercion by specifying a method.
Consider:
var arr = [1,2,3,4,5];
arr + 10; // 1,2,3,4,510
arr[Symbol.toPrimitive] = function(hint) {
if (hint == "default" || hint == "number") {
// sum all numbers
return this.reduce( function(acc,curr){
return acc + curr;
}, 0 );
}
};
arr + 10; // 25
The Symbol.toPrimitive
method will be provided with a hint of "string"
, "number"
, or "default"
(which should be interpreted as "number"
), depending on what type the operation invoking ToPrimitive
is expecting. In the previous snippet, the additive +
operation has no hint ("default"
is passed). A multiplicative *
operation would hint "number"
and a String(arr)
would hint "string"
.
Warning: The ==
operator will invoke the ToPrimitive
operation with no hint — the @@toPrimitive
method, if any is called with hint "default"
— on an object if the other value being compared is not an object. However, if both comparison values are objects, the behavior of ==
is identical to ===
, which is that the references themselves are directly compared. In this case, @@toPrimitive
is not invoked at all. See the Types & Grammar title of this series for more information about coercion and the abstract operations.
Regular Expression Symbols
There are four well known symbols that can be overridden for regular expression objects, which control how those regular expressions are used by the four corresponding String.prototype
functions of the same name:
@@match
: TheSymbol.match
value of a regular expression is the method used to match all or part of a string value with the given regular expression. It’s used byString.prototype.match(..)
if you pass it a regular expression for the pattern matching.The default algorithm for matching is laid out in section 21.2.5.6 of the ES6 specification (http://www.ecma-international.org/ecma-262/6.0/#sec-regexp.prototype-@@match). You could override this default algorithm and provide extra regex features, such as look-behind assertions.
Symbol.match
is also used by theisRegExp
abstract operation (see the note in “String Inspection Functions” in Chapter 6) to determine if an object is intended to be used as a regular expression. To force this check to fail for an object so it’s not treated as a regular expression, set theSymbol.match
value tofalse
(or something falsy).@@replace
: TheSymbol.replace
value of a regular expression is the method used byString.prototype.replace(..)
to replace within a string one or all occurrences of character sequences that match the given regular expression pattern.The default algorithm for replacing is laid out in section 21.2.5.8 of the ES6 specification (http://www.ecma-international.org/ecma-262/6.0/#sec-regexp.prototype-@@replace).
One cool use for overriding the default algorithm is to provide additional
replacer
argument options, such as supporting"abaca".replace(/a/g,[1,2,3])
producing"1b2c3"
by consuming the iterable for successive replacement values.@@search
: TheSymbol.search
value of a regular expression is the method used byString.prototype.search(..)
to search for a sub-string within another string as matched by the given regular expression.The default algorithm for searching is laid out in section 21.2.5.9 of the ES6 specification (http://www.ecma-international.org/ecma-262/6.0/#sec-regexp.prototype-@@search).
@@split
: TheSymbol.split
value of a regular expression is the method used byString.prototype.split(..)
to split a string into sub-strings at the location(s) of the delimiter as matched by the given regular expression.The default algorithm for splitting is laid out in section 21.2.5.11 of the ES6 specification (http://www.ecma-international.org/ecma-262/6.0/#sec-regexp.prototype-@@split).
Overriding the built-in regular expression algorithms is not for the faint of heart! JS ships with a highly optimized regular expression engine, so your own user code will likely be a lot slower. This kind of meta programming is neat and powerful, but it should only be used in cases where it’s really necessary or beneficial.
Symbol.isConcatSpreadable
The @@isConcatSpreadable
symbol can be defined as a boolean property (Symbol.isConcatSpreadable
) on any object (like an array or other iterable) to indicate if it should be spread out if passed to an array concat(..)
.
Consider:
var a = [1,2,3],
b = [4,5,6];
b[Symbol.isConcatSpreadable] = false;
[].concat( a, b ); // [1,2,3,[4,5,6]]
Symbol.unscopables
The @@unscopables
symbol can be defined as an object property (Symbol.unscopables
) on any object to indicate which properties can and cannot be exposed as lexical variables in a with
statement.
Consider:
var o = { a:1, b:2, c:3 },
a = 10, b = 20, c = 30;
o[Symbol.unscopables] = {
a: false,
b: true,
c: false
};
with (o) {
console.log( a, b, c ); // 1 20 3
}
A true
in the @@unscopables
object indicates the property should be unscopable, and thus filtered out from the lexical scope variables. false
means it’s OK to be included in the lexical scope variables.
Warning: The with
statement is disallowed entirely in strict
mode, and as such should be considered deprecated from the language. Don’t use it. See the Scope & Closures title of this series for more information. Because with
should be avoided, the @@unscopables
symbol is also moot.