Строки JavaScript вне BMP

BMP представляет собой базовый многоязычный уровень

Согласно JavaScript: преимущества:

JavaScript был создан в то время, когда Unicode был 16-битным набором символов, поэтому все символы в JavaScript имеют ширину 16 бит.

Это наводит меня на мысль, что JavaScript использует UCS-2 (не UTF-16!) и может обрабатывать только символы до U+FFFF.

Дальнейшее расследование подтверждает это:

> String.fromCharCode(0x20001);

Метод fromCharCode, по-видимому, использует только младшие 16 бит при возврате символа Unicode. Попытка получить U+20001 (унифицированная идеограмма CJK 20001) вместо этого возвращает U+0001.

Вопрос: возможно ли вообще обрабатывать символы пост-BMP в JavaScript?


31 июля 2011 г.: двенадцатый слайд из раздела Поддержка Unicode Shootout: Хорошее, плохое и (в основном) ужасное достаточно хорошо освещает связанные с этим вопросы:


person Delan Azabani    schedule 19.09.2010    source источник
comment
Если бы он использовал UTF-16, то можно было бы ожидать, что символы за пределами базовой многоязычной плоскости будут поддерживаться с использованием суррогатных пар. Почему вы ожидаете, что он примет 32-битный символ?   -  person Michael Aaron Safyan    schedule 19.09.2010
comment
Большое спасибо за это, я никогда не думал об этом в таком ключе.   -  person Delan Azabani    schedule 19.09.2010
comment
@MichaelAaronSafyan: Поскольку в JavaScript нет ничего похожего на тип char, а String.fromCharCode() возвращает строку, кажется справедливым ожидать, что он вернет строку, содержащую обе единицы кода, составляющие символ. Я считаю, что в будущий стандарт JavaScript будет добавлено String.fromCodePoint(), чтобы сделать именно это.   -  person hippietrail    schedule 30.05.2013
comment
Ваш вопрос объяснил, почему я буду продолжать получать length === 1 после использования String.fromCharCode   -  person Olga    schedule 23.05.2014
comment
Теперь вы можете сделать \u{23222} в ES6: D   -  person Henry    schedule 04.08.2016


Ответы (5)


Зависит от того, что вы подразумеваете под «поддержкой». Вы, конечно, можете поместить символы, отличные от UCS-2, в строку JS, используя суррогаты, и браузеры отобразят их, если смогут.

Но каждый элемент в строке JS — это отдельная единица кода UTF-16. Нет поддержки на уровне языка для обработки полных символов: все стандартные элементы String (length, split, slice и т. д.) имеют дело с кодовыми единицами, а не с символами, поэтому вполне успешно разбивают суррогатные пары или содержат недопустимые суррогатные последовательности.

Если вам нужны суррогатные методы, боюсь, вам придется начать писать их самостоятельно! Например:

String.prototype.getCodePointLength= function() {
    return this.length-this.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g).length+1;
};

String.fromCodePoint= function() {
    var chars= Array.prototype.slice.call(arguments);
    for (var i= chars.length; i-->0;) {
        var n = chars[i]-0x10000;
        if (n>=0)
            chars.splice(i, 1, 0xD800+(n>>10), 0xDC00+(n&0x3FF));
    }
    return String.fromCharCode.apply(null, chars);
};
person bobince    schedule 21.09.2010
comment
Большое Вам спасибо. Это отличный, подробный ответ. - person Delan Azabani; 21.09.2010
comment
@bobince Итак, технически JS использует UCS-2 или UTF-16? UCS-2 не поддерживает символы вне BMP, но JavaScript поддерживает, если вводятся отдельные суррогатные половинки отдельно (например, '\uD834\uDD1E' для U+1D11E). Но делает ли это UTF-16? - person Mathias Bynens; 16.01.2012
comment
@Mathias: JavaScript не знает UTF-16. Он дает вам последовательность 16-битных кодовых единиц и позволяет вам поместить в нее то, что вам нравится. Вы можете хранить в нем суррогаты, если хотите, но вы не получите никаких специальных функций для обработки их как символов. Хотите ли вы описать это как «использование» UCS-2 или UTF-16, это семантический аргумент, на который нет однозначного ответа. Однако, независимо от поддержки на уровне языка в JS, другие части браузера поддерживают суррогаты для рендеринга/взаимодействия в пользовательском интерфейсе, поэтому имеет смысл включать их в строки JS. - person bobince; 16.01.2012
comment
@bobince Спасибо! Я изучил это немного глубже и записал свои выводы здесь: mathiasbynens.be/notes/javascript-encoding Приветствуется обратная связь. - person Mathias Bynens; 20.01.2012
comment
(Обновлено из CodePoint, чтобы соответствовать имени, предложенному для поддержки ECMAScript 6 правильного Unicode. Теперь это фактически полифилл.) - person bobince; 28.01.2014
comment
Меняю свой голос, так как этот ответ устарел, говоря, что нет поддержки на уровне языка для обработки полных символов. Теперь есть полная поддержка символов на уровне языка, например, codePointAt, fromCodePoint, Array.from(), /u, for ... of, оператор .... Возможно, другие? - person hippietrail; 12.07.2017

Я пришел к тому же выводу, что и bobince. Если вы хотите работать со строками, содержащими символы Unicode за пределами BMP, вам необходимо переопределить методы String javascript. Это связано с тем, что javascript считает символы как каждое 16-битное кодовое значение. Символы, не входящие в BMP, должны быть представлены двумя кодовыми значениями. Таким образом, вы сталкиваетесь со случаем, когда некоторые символы считаются двумя символами, а некоторые — только одним.

Я повторно реализовал следующие методы для обработки каждой кодовой точки Юникода как одного символа: .length, .charCodeAt, .fromCharCode, .charAt, .indexOf, .lastIndexOf, .splice и .split.

Вы можете проверить это на jsfiddle: http://jsfiddle.net/Y89Du/

Вот код без комментариев. Я проверил его, но он может все еще иметь ошибки. Комментарии приветствуются.

if (!String.prototype.ucLength) {
    String.prototype.ucLength = function() {
        // this solution was taken from 
        // http://stackoverflow.com/questions/3744721/javascript-strings-outside-of-the-bmp
        return this.length - this.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g).length + 1;
    };
}

if (!String.prototype.codePointAt) {
    String.prototype.codePointAt = function (ucPos) {
        if (isNaN(ucPos)){
            ucPos = 0;
        }
        var str = String(this);
        var codePoint = null;
        var pairFound = false;
        var ucIndex = -1;
        var i = 0;  
        while (i < str.length){
            ucIndex += 1;
            var code = str.charCodeAt(i);
            var next = str.charCodeAt(i + 1);
            pairFound = (0xD800 <= code && code <= 0xDBFF && 0xDC00 <= next && next <= 0xDFFF);
            if (ucIndex == ucPos){
                codePoint = pairFound ? ((code - 0xD800) * 0x400) + (next - 0xDC00) + 0x10000 : code;
                break;
            } else{
                i += pairFound ? 2 : 1;
            }
        }
        return codePoint;
    };
}

if (!String.fromCodePoint) {
    String.fromCodePoint = function () {
        var strChars = [], codePoint, offset, codeValues, i;
        for (i = 0; i < arguments.length; ++i) {
            codePoint = arguments[i];
            offset = codePoint - 0x10000;
            if (codePoint > 0xFFFF){
                codeValues = [0xD800 + (offset >> 10), 0xDC00 + (offset & 0x3FF)];
            } else{
                codeValues = [codePoint];
            }
            strChars.push(String.fromCharCode.apply(null, codeValues));
        }
        return strChars.join("");
    };
}

if (!String.prototype.ucCharAt) {
    String.prototype.ucCharAt = function (ucIndex) {
        var str = String(this);
        var codePoint = str.codePointAt(ucIndex);
        var ucChar = String.fromCodePoint(codePoint);
        return ucChar;
    };
}

if (!String.prototype.ucIndexOf) {
    String.prototype.ucIndexOf = function (searchStr, ucStart) {
        if (isNaN(ucStart)){
            ucStart = 0;
        }
        if (ucStart < 0){
            ucStart = 0;
        }
        var str = String(this);
        var strUCLength = str.ucLength();
        searchStr = String(searchStr);
        var ucSearchLength = searchStr.ucLength();
        var i = ucStart;
        while (i < strUCLength){
            var ucSlice = str.ucSlice(i,i+ucSearchLength);
            if (ucSlice == searchStr){
                return i;
            }
            i++;
        }
        return -1;
    };
}

if (!String.prototype.ucLastIndexOf) {
    String.prototype.ucLastIndexOf = function (searchStr, ucStart) {
        var str = String(this);
        var strUCLength = str.ucLength();
        if (isNaN(ucStart)){
            ucStart = strUCLength - 1;
        }
        if (ucStart >= strUCLength){
            ucStart = strUCLength - 1;
        }
        searchStr = String(searchStr);
        var ucSearchLength = searchStr.ucLength();
        var i = ucStart;
        while (i >= 0){
            var ucSlice = str.ucSlice(i,i+ucSearchLength);
            if (ucSlice == searchStr){
                return i;
            }
            i--;
        }
        return -1;
    };
}

if (!String.prototype.ucSlice) {
    String.prototype.ucSlice = function (ucStart, ucStop) {
        var str = String(this);
        var strUCLength = str.ucLength();
        if (isNaN(ucStart)){
            ucStart = 0;
        }
        if (ucStart < 0){
            ucStart = strUCLength + ucStart;
            if (ucStart < 0){ ucStart = 0;}
        }
        if (typeof(ucStop) == 'undefined'){
            ucStop = strUCLength - 1;
        }
        if (ucStop < 0){
            ucStop = strUCLength + ucStop;
            if (ucStop < 0){ ucStop = 0;}
        }
        var ucChars = [];
        var i = ucStart;
        while (i < ucStop){
            ucChars.push(str.ucCharAt(i));
            i++;
        }
        return ucChars.join("");
    };
}

if (!String.prototype.ucSplit) {
    String.prototype.ucSplit = function (delimeter, limit) {
        var str = String(this);
        var strUCLength = str.ucLength();
        var ucChars = [];
        if (delimeter == ''){
            for (var i = 0; i < strUCLength; i++){
                ucChars.push(str.ucCharAt(i));
            }
            ucChars = ucChars.slice(0, 0 + limit);
        } else{
            ucChars = str.split(delimeter, limit);
        }
        return ucChars;
    };
}
person ecellingsworth    schedule 01.02.2013
comment
Большое спасибо за выпуск в общественное достояние. Вы, сэр/мадам, джентльмен/женщина и ученый. - person Ahmed Fasih; 12.11.2014
comment
ucCharAt кажется сломанным. "????????????????????".ucCharAt(0) возвращает правильное значение, но измените 0 на 1, и он вернет тарабарщину. Измените его на 2, и он вернет второй (вместо первого) символ. Таким образом, чтобы добраться до последнего символа, вы должны вызвать ucCharAt(8), который больше, чем ucLength строки. - person Ahmed Fasih; 12.11.2014

В более поздних движках JavaScript есть String.fromCodePoint.

const ideograph = String.fromCodePoint( 0x20001 ); // outside the BMP

Также итератор кодовой точки , что дает вам длину кодовой точки.

function countCodePoints( str )
{
    const i = str[Symbol.iterator]();
    let count = 0;
    while( !i.next().done ) ++count;
    return count;
}

console.log( ideograph.length ); // gives '2'
console.log( countCodePoints(ideograph) ); // '1'
person Michael Allan    schedule 24.12.2017

Да, ты можешь. Хотя поддержка символов, отличных от BMP, непосредственно в исходных документах не является обязательной в соответствии со стандартом ECMAScript, современные браузеры позволяют вам их использовать. Естественно, кодировка документа должна быть правильно объявлена, и для большинства практических целей вам потребуется использовать кодировку UTF-8. Кроме того, вам нужен редактор, который может обрабатывать UTF-8, и вам нужны некоторые методы ввода; см. напр. моя утилита Полный ввод Unicode.

Используя подходящие инструменты и настройки, вы можете написать var foo = '????'.

Не-BMP-символы будут внутренне представлены как суррогатные пары, поэтому каждый не-BMP-символ считается за 2 в длине строки.

person Jukka K. Korpela    schedule 10.12.2012

Используя инструкцию for (c of this), можно выполнять различные вычисления над строкой, содержащей символы, отличные от BMP. Например, чтобы вычислить длину строки и получить n-й символ строки:

String.prototype.magicLength = function()
{
    var c, k;
    k = 0;
    for (c of this) // iterate each char of this
    {
        k++;
    }
    return k;
}

String.prototype.magicCharAt = function(n)
{
    var c, k;
    k = 0;
    for (c of this) // iterate each char of this
    {
        if (k == n) return c + "";
        k++;
    }
    return "";
}
person Simon Hi    schedule 25.11.2018