String.prototype.search(): метод, который я хотел бы знать давно
Я пишу на JavaScript всего около 18 лет. Я начал когда-то в 2002 году, когда IE 6 был королем, Firefox только что вышел, а Chrome не существовало.
Я писал JavaScript почти два десятилетия, и я всегда был тем, кто любит копаться в документах, изучать каждую доступную функцию, каждый метод каждого объекта в браузере. Но иногда ... иногда я все еще, после всего этого времени, нахожу что-то, что было вокруг в течение долгого времени, и я просто не знал об этом.
Сегодня я обнаружил один такой метод: String.prototype.search()
. И мужик, я бы хотел знать об этом давно.
Что оно делает
Строковый метод .search()
довольно прост: это в основном .indexOf()
, но с одним существенным отличием: он использует регулярные выражения!
Вот демо со страницы MDN. Он демонстрирует, как вы можете найти первый непробельный, не буквенно-цифровой символ в строке:
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: "."
Это взорвало мой разум, когда я увидел это. Не потому, что это обязательно так безумно, а просто потому, что я никогда не знал, что это доступно для меня. Я взломал этот метод бесчисленное количество раз за эти годы, используя более грубый, менее читаемый String.prototype.match()
. Этот метод работает, и это мое подходящее решение, когда я хочу получить группы захвата и все такое, но просто найти индекс первого экземпляра определенного шаблона в строке так чисто .search(regex)
. С одной стороны, по крайней мере для меня, сразу очевидно, что здесь происходит, тогда как метод .match()
всегда занимал у меня минуту, чтобы понять. Для другого .match()
требуется дополнительная обработка, потому что он имеет три вида возвращаемых значений:
- если не найдено совпадение, возвращается
null
- если он находит совпадение:если у вашего регулярного выражения был глобальный флаг (
/.../g
как в примере выше MDN), он возвращает массив всех совпадений, и нет никакого способа получить их индексыесли у вашего регулярного выражения не было глобального флага, он возвращает объект со свойствомindex
Другой вариант, который я иногда использую, это RegExp.prototype.exec()
. Это имеет то преимущество, что он всегда возвращает объект со свойством index
, когда находит совпадение, независимо от глобального флага, но недостатком является то, что вам все равно нужно быть осторожным с глобальным флагом, если вы хотите запустить его на нескольких строках, потому что поиск начинается по индексу предыдущего совпадения. Иногда это может быть полезно, но не очень хорошо в простом случае.
Просто для того, чтобы прояснить этот вопрос, приведем параллельное сравнение:
// old way
const match = paragraph.match(regex)
const index = match ? match.index : -1
// new way
const index = paragraph.search(regex)
Как ES6 сделал его еще более мощным
То, как я столкнулся с String.prototype.search()
, было довольно забавно. Я просматривал README для фантастической библиотеки полифилла Пола Миллера ES6 Shim и заметил это в разделе «Предостережения» внизу:
- Известные
Symbol
Если для вас это не имеет смысла, давайте пройдем 30-секундный ускоренный курс по символам. Если это имеет смысл, пропустите следующий раздел.
Кратко о символах
Это будет очень краткий обзор, поэтому, если символы все еще не имеют для вас смысла после этого, я настоятельно рекомендую заняться поиском, потому что они очень важны для повышения уровня в JS (IMHO).
Символы - это новый примитивный тип, введенный в JavaScript в ECMAScript 2015, он же ES6. Основная идея, стоящая за ними, заключается в создании совершенно уникального ключа для использования в качестве имени свойства объекта, чтобы кто-то другой не мог случайно забить ваше свойство позже, используя то же имя, особенно для общих объектов и свойств глобального окна. До Символов было обычным делом видеть ключи на общих объектах с большим количеством начальных подчеркиваний, например ___myThing
, или со случайно сгенерированным префиксом, например 142857_myThing
. Это может показаться редким крайним случаем, если вы с ним не сталкивались, но поверьте мне, это много раз вызывало разочарование в истории JS.
Для ваших стандартных символов, созданных с помощью Symbol('foo')
, никто, кроме вас, не имеет к ним доступа, если вы не передадите их. Тем не менее, существует специальный набор так называемых «известных символов», к которым имеет доступ каждый. Вы можете создать свое собственное, зарегистрировав имя в глобальном реестре Symbol с помощью Symbol.for()
, как упомянуто в приведенной выше цитате, но также есть несколько известных символов, определенных браузером как свойства объекта Symbol. Они используются в качестве специальных имен свойств, которые обеспечивают определенные функциональные возможности для объектов.
Возможно, самым известным является Symbol.iterator
, что позволяет нам определять пользовательское поведение итерации для наших классов, которое затем используется синтаксисом распространения и [for ... of loop] для итерации по нашему объекту.
Вернемся к истории
После прочтения заметки в разделе «Предостережения» ES6 Shim мой вопрос был: «Для чего, черт возьми, этот Symbol.search
?» Я никогда раньше не сталкивался с этим известным символом, поэтому прочитал страницу MDN Symbol.search
, которая, в свою очередь, привела меня к этому String.prototype.search
.
Подводя итог, суть заключается в следующем: когда вы вызываете myString.seach(x)
, движок проверяет, есть ли у переменной, которую вы передали x
, метод, определенный под ключом [Symbol.search]
. Если нет, он пытается преобразовать ее в RegExp
вызвав new RegExp(x)
, который работает только для строк.
(Примечание: страница MDN вводит в заблуждение. В ней говорится: «Если передано регулярное выражение не-RegExp объекта, оно неявно преобразуется в RegExp с новым RegExp(regexp)». Но как мы увидим дальше, это не совсем верно; он не будет преобразован в RegExp, если вы передадите объект с [Symbol.search]
свойством.)
Для нас это означает, что мы можем написать собственную функцию поиска строк и обернуть ее в объект. Это может показаться нишевым, поскольку вы всегда можете просто передать строку в функцию, и это, безусловно, верно. Но что-то в синтаксисе мне нравится:
// 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))
Представьте, что вы просматриваете каталог файлов информации о клиентах в сценарии Node, пытаясь извлечь их имена, каждый раз используя один и тот же объект поиска, даже сохраняя искатель имен и аналогичные искатели для других полей в отдельном модуле и импортируя их. Я думаю, что это может быть аккуратно!