Symbols Are Your Friend Series
Since the wildly popular Symbols Are Your Friend article series has the momentum of a runaway freight train 🚂 (not really), let's check out some more static Symbol properties!
Symbol.search
Symbol.split
Symbol.species
Symbol.search
This symbol defines the method that returns the index of a regular expression within a string. It is called internally when String.prototype.search()
is used:
Default behavior:
'Wayne Gretzky: The Great One'.search(/Great/); // Returns 19
As you can see, String.search()
returns the index of the provided regular expression. We can modify this behavior with Symbol.search
:
const testString = 'Poke Tuna Meal: $10';
const priceRegEx = /\$/;
priceRegEx[Symbol.search] = function(str) {
const indexResult = (str.match(this) || []).index;
return `Position: ${indexResult || 'not found'}`;
};
testString.search(priceRegEx); // Returns "Position: 16"
'Water: FREE'.search(priceRegEx); // Returns "Position: not found"
Note that if you provide a string to String.search()
it will be implicitly converted to a Regular Expression thus allowing the use of Symbol.search
. The same applies to the next few static Symbol properties.
Symbol.split
Defines the method that splits a string at the indices that match a regular expression.
Default behavior:
'One Two Three'.split(' '); // Returns ["One", "Two", "Three"]
Symbol.split
modification:
const splitRegEx = / /;
splitRegEx[Symbol.split] = function(string) {
// Create copy of regex to prevent infinite loop
const regExCopy = new RegExp(this);
// Create modified result array
const array = string.split(regExCopy);
return array.map((item, index) => {
return `Char ${index + 1}: ${item}`;
});
};
'Yamcha Goku Vegeta'.split(splitRegEx);
/*
Returns:
["Char 1: Yamcha", "Char 2: Goku", "Char 3: Vegeta"]
*/
Symbol.species
This one's a bit tricky to wrap your head around. According to MDN, Symbol.species
specifies a function-valued property that the constructor function uses to create derived objects.
Essentially what this is saying is that Symbol.species
lets you change the default constructor of objects returned via methods on a "derived" class (a subclassed object).
For example, let's say we have a basic Fighter
class and an AdvancedFighter
class that extends Fighter
. Objects created via the AdvancedFighter
class will automatically inherit the Fighter
's prototype by way of the constructor. Additionally, subclasses of AdvancedFighter
will be instances of both AdvancedFighter
and Fighter
:
class Fighter {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
basicAttack() {
console.log(`${this.name}: Uses ${this.weapon} - 2 dmg`);
}
}
class AdvancedFighter extends Fighter {
advancedAttack() {
console.log(`${this.name}: Uses ${this.weapon} - 10 dmg`);
}
// Create a subclass object with the species we specified above
createSensei() {
return new this.constructor(this.name, this.weapon);
}
}
class Sensei {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
generateWisdom() {
console.log('Lost time is never found again.');
}
}
const splinter = new AdvancedFighter('Splinter', 'fists');
const splinterSensei = splinter.createSensei();
console.log(splinterSensei instanceof Fighter); // true
console.log(splinterSensei instanceof AdvancedFighter); // true
console.log(splinterSensei instanceof Sensei); // false
console.log(splinterSensei.basicAttack()); // ✅ Logs attack
console.log(splinterSensei.generateWisdom()); // ❌ TypeError
You can see in this code, we also created a Sensei
class. We can use Symbol.species
to specify AdvancedFighter
's derived classes to use the Sensei
constructor:
class Fighter {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
basicAttack() {
console.log(`${this.name}: Uses ${this.weapon} - 2 dmg`);
}
}
class AdvancedFighter extends Fighter {
// Override default constructor for subclasses
static get [Symbol.species]() { return Sensei; }
advancedAttack() {
console.log(`${this.name}: Uses ${this.weapon} - 10 dmg`);
}
// Create a subclass object with the species we specified above
createSensei() {
return new (this.constructor[Symbol.species] ||
this.constructor)(this.name, this.weapon);
}
}
class Sensei {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
generateWisdom() {
console.log('Lost time is never found again.');
}
}
const splinter = new AdvancedFighter('Splinter', 'fists');
const splinterSensei = splinter.createSensei();
console.log(splinterSensei instanceof Fighter); // false
console.log(splinterSensei instanceof AdvancedFighter); // false
console.log(splinterSensei instanceof Sensei); // true
console.log(splinterSensei.generateWisdom()); // ✅ Logs wisdom
console.log(splinterSensei.basicAttack()); // ❌ TypeError
The confusing part here is that Symbol.species
only specifies the constructor of subclass objects. These are created when a class method creates a new instance of a class with...
return new this.constructor();
if there is no defined species or:
return this.constructor[Symbol.species]();
if we've added a custom species getter.
We can combine some Symbol static property concepts together to illustrate this further:
class MyRegExp extends RegExp {
[Symbol.search](str) {
// Hack search() to return "this" (an instance of MyRegExp)
return new (this.constructor[Symbol.species] ||
this.constructor)();
}
}
const fooRegEx = new MyRegExp('foo');
const derivedObj = 'football'.search(fooRegEx);
console.log(derivedObj instanceof MyRegExp); // true
console.log(derivedObj instanceof RegExp); // true
class MyRegExp extends RegExp {
// Force MyRegExp subclasses to use the SpecialClass constructor
static get [Symbol.species]() { return SpecialClass; }
[Symbol.search](str) {
// Hack search() to return "this" (an instance of MyRegExp)
return new (this.constructor[Symbol.species] ||
this.constructor)();
}
}
class SpecialClass {
message() {
console.log('I\'m special!');
}
}
const fooRegEx = new MyRegExp('foo');
const derivedObj = 'football'.search(fooRegEx);
console.log(derivedObj instanceof MyRegExp); // false
console.log(derivedObj instanceof RegExp); // false
console.log(derivedObj instanceof SpecialClass); // true
derivedObj.message(); // Logs "I'm special!"
A potential use case for Symbol.species
would be if you wanted to create a custom API class object that includes all your internal / private methods but you wish for publicly created subclasses to use a different constructor.
See you in the next part! 👋
Check out more #JSBits at my blog, jsbits-yo.com. Or follow me on Twitter.
Top comments (0)