Если кратко: JavaScript не поддерживает суррогатные пары. Что в общем и правильно, так как в большинстве случаев это не нужно, а только лишние расходы. Однако, в меньшинстве случаев из-за этого придётся повозиться.
Теперь рассмотрим подробнее, что означает этот набор слов.
UTF-16 vs UCS-2
Как, наверняка, знают все здесь присутствующие, строки в компьютерах представляются, как последовательность байтиков. А каким образом эти байтики соответствуют буковкам, это определяет кодировка.
Старые добрые кодировки, вроде Windows-1251 были однобайтными. Одному символу — один байт. Работать с ними было одно удовольствие. Хочешь узнать длину строки (количество символов): это просто количество байт. Нужен 5-й символ: просто нужен пятый байт от начала строки. Единственный минус: в нашем многополярном мире 256 различных символов, это слишком мало. Особенно для китайцев с их тысячами иероглифов.
Поэтому теперь мы все любим Unicode. В юникоде много разных кодировок на все случаи жизни. Вот, например, UTF-8: в нём можно закодировать любой Unicode-символ хитрой переменного размера последовательностью байт. При этом базовая ASCII в нём остаётся такого же вида, как и в старых кодировках. Можно читать английские тексты не думая о кодировках, исходники программ и HTML/XML файлы сохраняют свою структуру, а наиболее популярные символы занимают меньше места, чем всякие иероглифы, что тоже приятно.
Неудобно его только обрабатывать. Чтобы узнать длину строки или найти N-й символ, нужно проходить по всем байтам сначала и производить над каждым разные вычисления. Что напрягает. Поэтому в памяти строки лучше хранить в какой-нибудь кодировке фиксированного размера.
Например, в UTF-32 (или UCS-4, что одно и тоже). Четыре байта на символ. Нужна длина: поделите количество байт на 4. Нужен N-й символ — умножьте на 4. Благодать. Единственный минус, тот же Firefox спокойно выедает гигабайт памяти. А если он ещё строки будет хранить по 4 байта на каждый символ, то вообще п.ц.
Поэтому лучше использовать UTF-16 (или UCS-2, что поначалу было одно и тоже). Те же радости фиксированной ширины и при этом вдвое меньше занятого места. Однако, количество различных символов сокращается до 2^16. Сначала это никого не волновало, ибо, как говорили древние: «64К хватит всем». Но в новых стандартах Unicode начали появляться такие обязательные к использованию символы, как мордочка обезьяны, улыбающийся кот с глазами сердечками и воняющий кусок говна.
Разработчики не устояли и придумали хак для того, чтобы с помощью UTF-16 можно было кодировать хотя бы миллион символов. Суррогатные пары. Есть «High Surrogates» (диапазон кодов D800—DB7F) и есть «Low Surrogates» (DC00—DFFF). Вместе верхний и нижний суррогат составляют суррогатную пару, которая кодирует символ выше базовой плоскости (дальше первых 64K). Итого такой символ занимает 4 байта. Например, вышеприведённый символ дымящегося «чего надо» представляется двумя суррогатами D83D
и DCA9
, которые путём хитрых манипуляций приводятся к итоговому Unicode-коду 1F4A9
.
С одной стороны в UTF-16 мы получили возможность использовать символ говна, с другой мы вернулись к переменной длине символа. И нам опять следует на каждый чих проходить всю строку сначала. В UCS-2 же суррогатные пары никак не обрабатываются и D8:3D:DC:A9
в нём будет просто двумя ничего не значащими символами.
В JavaScript
Стандарт ECMAScript по поводу того, какую кодировку следует использовать, как всегда темнит. В п.2. он заявляет «используйте UCS-2 или UTF-16, как вам нравится, хотя лучше UTF-16», а в 4.3.16 начинает что-то рассказывать про 16-битный integer, завершая своим любимым «стандарт не накладывает ограничений». Как обычно — делайте вы что хотите.
Переходим к делу. Мы захотели раскрутить Юникод на полную катушку и решили использовать символ фекалий в квадратных скобочках в своём нике на форуме:
В HTML это выглядит, например, так:
Автор: <div id="nick">[💩]</div> |
Содержимое тега div#nick
в честном UTF-16 будет представлено в виде последовательности байтов 00:5B:D8:3D:DC:A9:00:5D
. При разборе этого мы берём первое 16-битное слово: 005B
. Это открывающая скобочка, обычный символ. Дальше — D83D
, это начало суррогатной пары, дальше должна идти часть из «Low Surrogates» и она там есть: DCA9
. Значит склеиваем в один символ, преобразуя в код 1F4A9
. Итого у нас строка из трёх символов: 5B 1F4A9 5D
.
Если же мы разбираем эти байты с точки зрения UCS-2, то у нас просто набор 2-байтных слов: 005B D83D DCA9 005D
.
Попробуем:
var nick = document.getElementById("nick").firstChild.nodeValue, len = nick.length, i; console.log("length = " + len); for (i = 0; i < len; i += 1) { console.log(nick.charCodeAt(i).toString(16)); } |
Результат:
length = 4 5b d83d dca9 5d
То есть из DOM нам пришёл UTF-16, но JS работает с ним, как с UCS-2. Длина строки у нас вместо 3 стала 4, а если мы захотим получить третий символ, то вместо «]» мы получим хз что (DCA9).
Точно также если мы захотим вывести улыбающегося кота, зная его код (1F63B):
var cat = String.fromCharCode(0x1F63B); document.getElementById("nick").innerHTML = cat; console.log(cat.charCodeAt(0).toString(16)); // f63b |
Хрена с два. fromCharCode()
использует только младшие 16 бит.
Попаболь
Поэтому, если у вас есть вероятность попадания в ваши строки дымящихся кусков говна и вы хотите их корректно обрабатывать, то вам придётся самим реализовывать поддержку UTF-16 поверх UCS-2.
На сегодня всё.
Спасибо, было интересно почитать, пиши еще!
adw0rd, 9.02.2015, 13:00
Большое спасибо, очень интересно написали! Ахахаха))
Darmen, 7.09.2016, 17:59