Сравнение строк
Как мы знаем из главы Операторы сравнения, строки сравниваются посимвольно в алфавитном порядке.
Тем не менее, есть некоторые нюансы.
- Строчные буквы больше заглавных:
alert( 'a' > 'Z' ); // true
- Буквы, имеющие диакритические знаки, идут «не по порядку»:
alert( 'Österreich' > 'Zealand' ); // true
Это может привести к своеобразным результатам при сортировке названий стран: нормально было бы ожидать, чтоZealand
будет послеÖsterreich
в списке.
Чтобы разобраться, что происходит, давайте ознакомимся с внутренним представлением строк в JavaScript.
Строки кодируются в UTF-16. Таким образом, у любого символа есть соответствующий код. Есть специальные методы, позволяющие получить символ по его коду и наоборот.str.codePointAt(pos)
Возвращает код для символа, находящегося на позиции pos
:
// одна и та же буква в нижнем и верхнем регистре
// будет иметь разные коды
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90
String.fromCodePoint(code)
Создаёт символ по его коду code
alert( String.fromCodePoint(90) ); // Z
Также можно добавлять юникодные символы по их кодам, используя \u
с шестнадцатеричным кодом символа:
// 90 — 5a в шестнадцатеричной системе счисления
alert( '\u005a' ); // Z
Давайте сделаем строку, содержащую символы с кодами от 65
до 220
— это латиница и ещё некоторые распространённые символы:
let str = '';
for (let i = 65; i <= 220; i++) {
str += String.fromCodePoint(i);
}
alert( str );
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
// ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ
Как видите, сначала идут заглавные буквы, затем несколько спецсимволов, затем строчные и Ö
ближе к концу вывода.
Теперь очевидно, почему a > Z
.
Символы сравниваются по их кодам. Больший код — больший символ. Код a
(97) больше кода Z
(90).
- Все строчные буквы идут после заглавных, так как их коды больше.
- Некоторые буквы, такие как
Ö
, вообще находятся вне основного алфавита. У этой буквы код больше, чем у любой буквы отa
доz
.
Правильное сравнение
«Правильный» алгоритм сравнения строк сложнее, чем может показаться, так как разные языки используют разные алфавиты.
Поэтому браузеру нужно знать, какой язык использовать для сравнения.
К счастью, все современные браузеры (для IE10− нужна дополнительная библиотека Intl.JS) поддерживают стандарт ECMA 402, обеспечивающий правильное сравнение строк на разных языках с учётом их правил.
Для этого есть соответствующий метод.
Вызов str.localeCompare(str2) возвращает число, которое показывает, какая строка больше в соответствии с правилами языка:
- Отрицательное число, если
str
меньшеstr2
. - Положительное число, если
str
большеstr2
. 0
, если строки равны.
Например:
alert( 'Österreich'.localeCompare('Zealand') ); // -1
У этого метода есть два дополнительных аргумента, которые указаны в документации. Первый позволяет указать язык (по умолчанию берётся из окружения) — от него зависит порядок букв. Второй — определить дополнительные правила, такие как чувствительность к регистру, а также следует ли учитывать различия между "a"
и "á"
.
Как всё устроено, Юникод
Глубокое погружение в тему
Этот раздел более подробно описывает, как устроены строки. Такие знания пригодятся, если вы намерены работать с эмодзи, редкими математическими символами, иероглифами, либо с ещё какими-то редкими символами.
Если вы не планируете их поддерживать, эту секцию можно пропустить.
Суррогатные пары
Многие символы возможно записать одним 16-битным словом: это и буквы большинства европейских языков, и числа, и даже многие иероглифы.
Но 16 битов — это 65536 комбинаций, так что на все символы этого, разумеется, не хватит. Поэтому редкие символы записываются двумя 16-битными словами — это также называется «суррогатная пара».
Длина таких строк — 2
:
alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X
alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY
alert( '𩷶'.length ); // 2, редкий китайский иероглиф
Обратите внимание, суррогатные пары не существовали, когда был создан JavaScript, поэтому язык не обрабатывает их адекватно!
Ведь в каждой из этих строк только один символ, а length
показывает длину 2
.
String.fromCodePoint
и str.codePointAt
— два редких метода, правильно работающие с суррогатными парами, но они и появились в языке недавно. До них были только String.fromCharCode и str.charCodeAt. Эти методы, вообще, делают то же самое, что fromCodePoint/codePointAt
, но не работают с суррогатными парами.
Получить символ, представленный суррогатной парой, может быть не так просто, потому что суррогатная пара интерпретируется как два символа:
alert( '𝒳'[0] ); // странные символы…
alert( '𝒳'[1] ); // …части суррогатной пары
Части суррогатной пары не имеют смысла сами по себе, так что вызовы alert
в этом примере покажут лишь мусор.
Технически, суррогатные пары возможно обнаружить по их кодам: если код символа находится в диапазоне 0xd800..0xdbff
, то это — первая часть суррогатной пары. Следующий символ — вторая часть — имеет код в диапазоне 0xdc00..0xdfff
. Эти два диапазона выделены исключительно для суррогатных пар по стандарту.
В данном случае:
// charCodeAt не поддерживает суррогатные пары, поэтому возвращает код для их частей
alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, между 0xd800 и 0xdbff
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, между 0xdc00 и 0xdfff
Дальше в главе Перебираемые объекты будут ещё способы работы с суррогатными парами. Для этого есть и специальные библиотеки, но нет достаточно широко известной, чтобы предложить её здесь.
Диакритические знаки и нормализация
Во многих языках есть символы, состоящие из некоторого основного символа со знаком сверху или снизу.
Например, буква a
— это основа для àáâäãåā
. Наиболее используемые составные символы имеют свой собственный код в таблице UTF-16. Но не все, в силу большого количества комбинаций.
Чтобы поддерживать любые комбинации, UTF-16 позволяет использовать несколько юникодных символов: основной и дальше один или несколько особых символов-знаков.
Например, если после S
добавить специальный символ «точка сверху» (код \u0307
), отобразится Ṡ.
alert( 'S\u0307' ); // Ṡ
Если надо добавить сверху (или снизу) ещё один знак — без проблем, просто добавляем соответствующий символ.
Например, если добавить символ «точка снизу» (код \u0323
), отобразится S с точками сверху и снизу: Ṩ
.
Добавляем два символа:
alert( 'S\u0307\u0323' ); // Ṩ
Это даёт большую гибкость, но из-за того, что порядок дополнительных символов может быть различным, мы получаем проблему сравнения символов: можно представить по-разному символы, которые ничем визуально не отличаются.
Например:
let s1 = 'S\u0307\u0323'; // Ṩ, S + точка сверху + точка снизу
let s2 = 'S\u0323\u0307'; // Ṩ, S + точка снизу + точка сверху
alert( `s1: ${s1}, s2: ${s2}` );
alert( s1 == s2 ); // false, хотя на вид символы одинаковы (?!)
Для решения этой проблемы есть алгоритм «юникодной нормализации», приводящий каждую строку к единому «нормальному» виду.
Его реализует метод str.normalize().
alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true
Забавно, но в нашем случае normalize()
«схлопывает» последовательность из трёх символов в один: \u1e68
— S с двумя точками.
alert( "S\u0307\u0323".normalize().length ); // 1
alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true
Разумеется, так происходит не всегда. Просто Ṩ — это достаточно часто используемый символ, поэтому создатели UTF-16 включили его в основную таблицу и присвоили ему код.
Подробнее о правилах нормализации и составлении символов можно прочитать в дополнении к стандарту Юникод: Unicode Normalization Forms. Для большинства практических целей информации из этого раздела достаточно.
Итого
- Есть три типа кавычек. Строки, использующие обратные кавычки, могут занимать более одной строки в коде и включать выражения
${…}
. - Строки в JavaScript кодируются в UTF-16.
- Есть специальные символы, такие как
\n
, и можно добавить символ по его юникодному коду, используя\u…
. - Для получения символа используйте
[]
. - Для получения подстроки используйте
slice
илиsubstring
. - Для того, чтобы перевести строку в нижний или верхний регистр, используйте
toLowerCase/toUpperCase
. - Для поиска подстроки используйте
indexOf
илиincludes/startsWith/endsWith
, когда надо только проверить, есть ли вхождение. - Чтобы сравнить строки с учётом правил языка, используйте
localeCompare
.
Строки также имеют ещё кое-какие полезные методы:
str.trim()
— убирает пробелы в начале и конце строки.str.repeat(n)
— повторяет строкуn
раз.- …и другие, которые вы можете найти в справочнике.
Также есть методы для поиска и замены с использованием регулярных выражений. Но это отдельная большая тема, поэтому ей посвящена отдельная глава учебника Регулярные выражения.