Почему кодовые точки между U+D800 и U+DBFF генерируют строку одной длины в ECMAScript 6?
Я слишком запутался. Почему кодовые точки от U+D800 до U+DBFF кодируются как один (2 байта) строковый элемент при использовании встроенных помощников Unicode в ECMAScript 6?
Я не спрашиваю, как JavaScript/ECMAScript изначально кодирует строки, я спрашиваю о дополнительной функциональности для кодирования UTF-16, которая использует UCS-2.
var str1 = '\u{D800}';
var str2 = String.fromCodePoint(0xD800);
console.log(
str1.length, str1.charCodeAt(0), str1.charCodeAt(1)
);
console.log(
str2.length, str2.charCodeAt(0), str2.charCodeAt(1)
);
Re- TL; DR: я хочу знать, почему вышеупомянутые подходы возвращают строку длины 1
, Не должен U+D800 генерировать 2
длина строки, так как реализация моего браузера ES6 включает в себя кодирование UCS-2 в строках, который использует 2 байта для каждого кода символа?
Оба эти подхода возвращают одноэлементную строку для кодовой точки U+D800 (код символа: 55296
, такой же как 0xD800
). Но для кодовых точек больше, чем U + FFFF, каждая возвращает двухэлементную строку, ведущую и трейл. лидерство будет числом между U+D800 и U+DBFF, и трейл, в котором я не уверен, я только знаю, что это помогает изменить точку кода результата. Для меня возвращаемое значение не имеет смысла, оно представляет лидерство без следа. Я что-то не так понимаю?
2 ответа
Я думаю, что ваша путаница связана с тем, как работают кодировки Unicode в целом, поэтому позвольте мне объяснить.
Сам Unicode просто указывает список символов, называемых "кодовыми точками", в определенном порядке. Он не говорит вам, как преобразовать их в биты, он просто дает им все числа от 0 до 1114111 (в шестнадцатеричном, 0x10FFFF). Есть несколько различных способов, которыми эти числа от U+0 до U+10FFFF могут быть представлены как биты.
В более ранней версии ожидалось, что диапазона от 0 до 65535 (0xFFFF) будет достаточно. Это может быть естественно представлено в 16 битах, используя то же соглашение, что и целое число без знака. Это был оригинальный способ хранения Unicode, и теперь он известен как UCS-2. Чтобы сохранить одну кодовую точку, вы резервируете 16 бит памяти.
Позже было решено, что этот диапазон не был достаточно большим; это означало, что были кодовые точки выше 65535, которые вы не можете представить в 16-битном фрагменте памяти. UTF-16 был изобретен как умный способ хранения этих более высоких кодовых точек. Он работает следующим образом: "Если вы посмотрите на 16-битный фрагмент памяти, и это число от 0xD800 до 0xDBF (" низкий суррогат "), то вам также нужно взглянуть на следующие 16 битов памяти". Любая часть кода, которая выполняет эту дополнительную проверку, обрабатывает свои данные как UTF-16, а не UCS-2.
Важно понимать, что сама память не "знает", в какой она кодировке, разница между UCS-2 и UTF-16 заключается в том, как вы интерпретируете эту память. Когда вы пишете часть программного обеспечения, вы должны выбрать, какую интерпретацию вы собираетесь использовать.
Теперь на Javascript...
Javascript обрабатывает ввод и вывод строк, интерпретируя его внутреннее представление как UTF-16. Это здорово, это означает, что вы можете набирать и отображать известного персонажа, который не может быть сохранен в одном 16-битном фрагменте памяти.
Проблема в том, что большинство встроенных строковых функций фактически обрабатывают данные как UCS-2, то есть они смотрят на 16 бит за раз, и им все равно, является ли то, что они видят, специальным "суррогатом". Функция, которую вы использовали, charCodeAt()
Примером этого является: он считывает 16 бит из памяти и выдает их вам в виде числа от 0 до 65535. Если вы передадите его, он просто вернет вам первые 16 бит; спросите его о следующем "символе" после, и он даст вам вторые 16 бит (которые будут "высоким суррогатом", между 0xDC00 и 0xDFFF).
В ECMAScript 6 (2015) была добавлена новая функция: codePointAt()
, Вместо того, чтобы просто смотреть на 16 бит и выдавать их вам, эта функция проверяет, представляют ли они одну из единиц суррогатного кода UTF-16, и, если так, ищет "другую половину" - так что она дает вам число от 0 до 1114111. Если вы кормите его, он правильно даст вам 128169.
var poop = '';
console.log('Treat it as UCS-2, two 16-bit numbers: ' + poop.charCodeAt(0) + ' and ' + poop.charCodeAt(1));
console.log('Treat it as UTF-16, one value cleverly encoded in 32 bits: ' + poop.codePointAt(0));
// The surrogates are 55357 and 56489, which encode 128169 as follows:
// 0x010000 + ((55357 - 0xD800) << 10) + (56489 - 0xDC00) = 128169
Ваш отредактированный вопрос теперь спрашивает это:
Я хочу знать, почему вышеупомянутые подходы возвращают строку длины 1. Разве U+D800 не должен генерировать строку длины 2?
Шестнадцатеричное значение D800 равно 55296 в десятичном виде, что меньше 65536, поэтому, учитывая все, что я сказал выше, это хорошо вписывается в 16 бит памяти. Так что, если мы спросим charCodeAt
читать 16 бит памяти, и он находит это число там, это не будет иметь проблемы.
Точно так же .length
Свойство измеряет количество наборов из 16 битов в строке. Поскольку эта строка хранится в 16 битах памяти, нет никаких оснований ожидать какой-либо длины, кроме 1.
Единственная необычная вещь в этом числе - то, что в Юникоде это значение зарезервировано - нет и не будет символа U+D800. Это потому, что это одно из магических чисел, которое говорит алгоритму UTF-16 "это только половина символа". Таким образом, возможно, что любая попытка создать эту строку будет просто ошибкой - например, открытие пары скобок, которые вы никогда не закроете, будет несбалансированным, неполным.
Единственный способ получить строку длины 2 - если двигатель каким-то образом угадал, какой должна быть вторая половина; но как это узнать? Есть 1024 варианта, от 0xDC00 до 0xDFFF, которые можно включить в формулу, показанную выше. Так что он не догадывается, и, поскольку он не ошибается, полученная строка имеет длину 16 бит.
Конечно, вы можете поставить соответствующие половинки, и codePointAt
будет интерпретировать их для вас.
// Set up two 16-bit pieces of memory
var high=String.fromCharCode(55357), low=String.fromCharCode(56489);
// Note: String.fromCodePoint will give the same answer
// Glue them together (this + is string concatenation, not number addition)
var poop = high + low;
// Read out the memory as UTF-16
console.log(poop);
console.log(poop.codePointAt(0));
Ну, это происходит потому, что в спецификации сказано:
Вместе эти двое говорят, что если аргумент < 0
или же > 0x10FFFF
, RangeError
брошен, но в остальном любая кодовая точка <= 65535
включается в строку результата как есть.
Что касается того, почему все так указано, я не знаю. Похоже, что JavaScript на самом деле не поддерживает Unicode, только UCS-2.
Unicode.org может сказать следующее по этому вопросу:
http://www.unicode.org/faq/utf_bom.html
Q: Что такое суррогаты?
A: Суррогаты - это кодовые точки из двух специальных диапазонов значений Unicode, зарезервированные для использования в качестве начальных и конечных значений парных единиц кода в UTF-16. Ведущие, также называемые высокими, суррогаты составляют от D80016 до DBFF16, а конечные или низкие суррогаты - от DC0016 до DFFF16. Их называют суррогатами, поскольку они не представляют персонажей напрямую, а только в виде пары.
http://www.unicode.org/faq/utf_bom.html
Q: Есть ли 16-битные значения, которые являются недопустимыми?
A: Непарные суррогаты недействительны в UTF. Они включают любое значение в диапазоне от D80016 до DBFF16, за которым не следует значение в диапазоне от DC0016 до DFFF16, или любое значение в диапазоне от DC0016 до DFFF16, которому не предшествует значение в диапазоне от D80016 до DBFF16,
Поэтому результат String.fromCodePoint
не всегда действует UTF-16, потому что он может испускать непарные суррогаты.