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:

  1. var arr = [4,5,6,7,8,9];
  2. for (var v of arr) {
  3. console.log( v );
  4. }
  5. // 4 5 6 7 8 9
  6. // define iterator that only produces values
  7. // from odd indexes
  8. arr[Symbol.iterator] = function*() {
  9. var idx = 1;
  10. do {
  11. yield this[idx];
  12. } while ((idx += 2) < this.length);
  13. };
  14. for (var v of arr) {
  15. console.log( v );
  16. }
  17. // 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:

  1. function Foo() {}
  2. var a = new Foo();
  3. a.toString(); // [object Object]
  4. a instanceof Foo; // true

As of ES6, you can control the behavior of these operations:

  1. function Foo(greeting) {
  2. this.greeting = greeting;
  3. }
  4. Foo.prototype[Symbol.toStringTag] = "Foo";
  5. Object.defineProperty( Foo, Symbol.hasInstance, {
  6. value: function(inst) {
  7. return inst.greeting == "hello";
  8. }
  9. } );
  10. var a = new Foo( "hello" ),
  11. b = new Foo( "world" );
  12. b[Symbol.toStringTag] = "cool";
  13. a.toString(); // [object Foo]
  14. String( b ); // [object cool]
  15. a instanceof Foo; // true
  16. 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:

  1. class Cool {
  2. // defer `@@species` to derived constructor
  3. static get [Symbol.species]() { return this; }
  4. again() {
  5. return new this.constructor[Symbol.species]();
  6. }
  7. }
  8. class Fun extends Cool {}
  9. class Awesome extends Cool {
  10. // force `@@species` to be parent constructor
  11. static get [Symbol.species]() { return Cool; }
  12. }
  13. var a = new Fun(),
  14. b = new Awesome(),
  15. c = a.again(),
  16. d = b.again();
  17. c instanceof Fun; // true
  18. d instanceof Awesome; // false
  19. 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:

  1. var arr = [1,2,3,4,5];
  2. arr + 10; // 1,2,3,4,510
  3. arr[Symbol.toPrimitive] = function(hint) {
  4. if (hint == "default" || hint == "number") {
  5. // sum all numbers
  6. return this.reduce( function(acc,curr){
  7. return acc + curr;
  8. }, 0 );
  9. }
  10. };
  11. 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: The Symbol.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 by String.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 the isRegExp 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 the Symbol.match value to false (or something falsy).

  • @@replace: The Symbol.replace value of a regular expression is the method used by String.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: The Symbol.search value of a regular expression is the method used by String.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: The Symbol.split value of a regular expression is the method used by String.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:

  1. var a = [1,2,3],
  2. b = [4,5,6];
  3. b[Symbol.isConcatSpreadable] = false;
  4. [].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:

  1. var o = { a:1, b:2, c:3 },
  2. a = 10, b = 20, c = 30;
  3. o[Symbol.unscopables] = {
  4. a: false,
  5. b: true,
  6. c: false
  7. };
  8. with (o) {
  9. console.log( a, b, c ); // 1 20 3
  10. }

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.