String.prototype.search(): a method I wish I knew about a long time ago
Tags: javascript, todayilearned, todayisearched
Published: 2020-06-23 17:13:03

**tl;dr:**  [`String.prototype.search()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search) is basically `.indexOf()` but with regexes. It's been supported in every browser since IE 4, but ES6 made it more powerful with [`Symbol.search`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/search).

----

I have been writing JavaScript for just about 18 years. I started sometime in 2002, when IE 6 was king, Firefox was just being released, and Chrome did not exist.

I've been writing JavaScript for almost two decades, and I've always been someone who loves digging into the docs, learning every feature available, every method of every object in the browser. But sometimes... sometimes I *still*, after all this time, find something that's been around for a long time and I just didn't know about.

Today I discovered one such method: `String.prototype.search()`. And man, I *wish* I had known about this one a loooong time ago.

## What it does

The `.search()` string method is pretty straightforward: as I mentioned in the tl;dr, it's basically `.indexOf()`, but with one crucial difference: **it uses regular expressions!**

Here's the demo from [the MDN page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search). It demonstrates how you would find the first non-whitespace, non-alphanumeric character in a string:
```js
const paragraph = 'The quick brown fox jumps over the lazy dog. If the dog barked, was it really lazy?';

// any character that is not a word character or whitespace
const regex = /[^\w\s]/g;

console.log(paragraph.search(regex));
// expected output: 43

console.log(paragraph[paragraph.search(regex)]);
// expected output: "."
```

This blew my mind when I saw it. Not because it's necessarily that crazy, but just because I never knew it was available to me. I have hacked together this method countless times over the years using the clunkier, less readable `String.prototype.match()`. This method works, and it's my go-to solution when I want capture groups and all that, but for simply finding the index of the first instance of a certain pattern in a string, `.search(regex)` is just so *clean*. For one, to me at least, it's immediately obvious what's happening here, whereas the `.match()` method always took me a minute to understand. For another, `.match()` requires extra processing, because it has three kinds of return values:
  * if it doesn't find a match, it returns `null`
  * if it finds a match:
    * if your regex had the global flag (`/.../g`, like in MDN's example above), it returns an array of all matches, and there's no way to get their indices
    * if your regex did not have the global flag, it returns an object with an `index` property

So `.match()` gets complicated.

Another option that I sometimes use is `RegExp.prototype.exec()`. This has the advantage that it always returns an object with an `index` property when it finds a match, regardless of the global flag, but the disadvantage that you still need to be careful about the global flag if you want to run it on multiple strings, because it starts searching from the index of the previous match. This can be useful sometimes, but isn't great in the simple case.

Just to drive this point home, here's the side-by-side comparison:
```js
// old way
const match = paragraph.match(regex)
const index = match ? match.index : -1

// new way
const index = paragraph.search(regex)
```

I don't know. I get really excited about stuff like this. Maybe you don't. But if that didn't excite you, maybe this will:

## How ES6 made it even more powerful

The way I came across `String.prototype.search()` was kind of funny. I was looking over the README for Paul Miller's fantastic polyfill library, [ES6 Shim](https://github.com/paulmillr/es6-shim), and I noticed this in [the "Caveats" section](https://github.com/paulmillr/es6-shim#caveats) at the bottom:

> * Well-known `Symbol`s
>   * In order to make them work cross-realm, these are created with the global `Symbol` registry via `Symbol.for`. This does not violate the spec, but it does mean that `Symbol.for('Symbol.search') === Symbol.search` will be `true`, which it would not by default in a fresh compliant realm.

If that makes no sense to you, let's do a 30-second crash course on Symbols. If it did make sense, skip the next section.

### A brief aside about Symbols 

This will be a very quick overview, so if Symbols still don't make a ton of sense to you after this, I highly recommend doing some googling, because they're pretty important for leveling up in JS (IMHO).

Symbols are a new primitive type introduced to JavaScript in ECMAScript 2015, a.k.a. ES6. The basic idea behind them is to create a perfectly unique key to use as an object property name, so that it's impossible for someone else to accidentally clobber your property later by using the same name, especially on shared objects and global window properties. Before Symbols, it was common to see keys on shared objects with lots of leading underscores, stuff like `___myThing`, or with a randomly generated prefix, like `142857_myThing`. This may seem like a rare edge case if you haven't encountered it, but trust me, this has been a source of frustration many times in JS history.

For your standard, garden-variety Symbols, created with `Symbol('foo')`, no one but you has access to them unless you pass them around. However, there is a special set of so-called "well-known Symbols" that everyone has access to. You can create your own by registering a name in the global Symbol registry with `Symbol.for()`, as mentioned in the quote above, but there are also several well-known symbols defined by the browser as properties on the Symbol object. These are used as special property names that enable certain functionality for objects.

Perhaps the most famous is [`Symbol.iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator), which lets us define custom iteration behavior for our classes, which is then used by [the spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) and the [for ... of loop] to iterate over our object. I wrote a whole post about ES6 iterators and how they relate to generators a while back, if you're interested in a deep dive on this topic (it gets pretty wild when you dig deep):
{% link https://dev.to/kenbellows/the-javascript-iteration-protocols-and-how-they-fit-in-1g8i %}

Okay, hopefully we all have at least enough understanding to follow the rest of the story here.

### Back to the story

After reading the note in the Caveats section of ES6 Shim, my question was, "What the heck is `Symbol.search` for?" I had never encountered this particular well-known Symbol before, so I read [the MDN page on `Symbol.search`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/search), which in turn led me to `String.prototype.search`.

I've already gotten a bit long-winded here, so to wrap up quickly, the bottom line is this: when you call `myString.seach(x)`, the engine checks whether the thing you passed in, `x`, has a method defined under the key `[Symbol.search]`. If not, it tries to convert to a `RegExp` by calling `new RegExp(x)`, which only works for strings.

(**Side note:** The MDN page is misleading here. It says: "If a non-RegExp object regexp is passed, it is implicitly converted to a RegExp with new RegExp(regexp)." But as we'll see next, this is not strictly true; it will not convert to a RegExp if you pass an object with a `[Symbol.search]` property.)

So what this means for us is that we can write a custom string search function and wrap it in an object. This may seem niche, since you can always just pass the string to the function, and this is certainly true. But something about the syntax feels nice to me:

```js
// Find the index of the first character following a string like:
//    "Name:\t"
const nameFinder = {
  [Symbol.search](s) {
    const result = /Name:\s*/.exec(s)
    if (result) {
      const {0: label, index} = result
      return index + label.length
    }
    else {
      return -1
    }
  }
}

// imagine this was read in from a file
const doc = `Customer Information
ID:	11223344
Name:	John Smith
Address:	123 Main Street
...`

const customerNameStart = doc.search(nameFinder)
const customerName = doc.slice(customerNameStart, doc.indexOf('\n', customerNameStart))
```

Imagine looping over a directory of customer info files in a Node script trying to extract their names, reusing this same search object each time, even storing the name finder and similar finders for other fields in a separate module and importing them. I think it could be neat! (Just me?)

## Conclusion

Honestly, I recognize that this is not super revolutionary or anything, and it probably won't change a lot of workflows. But to me, that isn't the important thing; what's most important to me is to know what tools are available. I honestly don't know when I would use a customer search object like the one above, but I think it's very cool that it's an option. And now that that I know about it, if I ever come across a situation where it really is useful, I'll have it in the back of my head. It's another Batarang on my utility belt.

(Also, I just think metaprogramming stuff like this is really cool 😎)

----

## Endnote

Thanks if you read all this! It's niche, I know, and I get more excited than most devs I know about little things like this. But if you got excited about this article, let me know in the comments, or shoot me a DM!