BMP 之外的 JavaScript 字符串

IT技术 javascript unicode utf-16 surrogate-pairs astral-plane
2021-01-13 09:04:52

BMP是基本的多语言平面

根据JavaScript:好的部分

JavaScript 是在 Unicode 是 16 位字符集的时候构建的,因此 JavaScript 中的所有字符都是 16 位宽。

这让我相信 JavaScript 使用 UCS-2(不是 UTF-16!)并且只能处理高达 U+FFFF 的字符。

进一步的调查证实了这一点:

> String.fromCharCode(0x20001);

fromCharCode方法在返回 Unicode 字符时似乎只使用最低 16 位。尝试获取 U+20001(CJK 统一表意文字 20001)反而返回 U+0001。

问题:是否有可能在 JavaScript 中处理 BMP 后的字符?


2011-07-31:Unicode Support Shootout 中的12 张幻灯片好的、坏的和(大部分)丑陋的内容很好地涵盖了与此相关的问题:

5个回答

取决于你所说的“支持”是什么意思。您当然可以使用代理将非 UCS-2 字符放入 JS 字符串中,如果可以,浏览器会显示它们。

但是,JS 字符串中的每一项都是一个单独的 UTF-16 代码单元。没有处理全字符的语言层面的支持:所有标准字符串成员(lengthsplitslice等)都处理代码单元没有字符,所以会很愉快地拆分代理对或持有无效的替代序列。

如果您想要代理感知方法,恐怕您将不得不自己开始编写它们!例如:

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);
};
(从CodePoint 更新以匹配为ECMAScript 6 支持正确Unicode 所提议的名称。现在这实际上是一个polyfill。)
2021-03-15 09:04:52
@bobince 谢谢!我进一步研究了一下,并在此处记录了我的发现:mathiasbynens.be/notes/javascript-encoding欢迎反馈。
2021-03-26 09:04:52
非常感谢你。这是一个很好的,详细的答案。
2021-03-27 09:04:52
@bobince 那么,从技术上讲,JS 使用 UCS-2 还是 UTF-16?UCS-2 不支持 BMP 之外的字符,但如果单独输入各个代理项(例如'\uD834\uDD1E'U+1D11E),JavaScript 会支持但这是否使它成为 UTF-16?
2021-03-27 09:04:52
@Mathias:JavaScript 不了解 UTF-16。它为您提供了一系列 16 位代码单元,并允许您将喜欢的内容放入其中。如果需要,您可以在其中存储代理,但您不会获得任何特殊功能来将它们作为字符处理。您是否想将其描述为“使用”UCS-2 或 UTF-16 是一种语义论点,对此没有一个明确的答案。然而,不管 JS 中的语言级别支持如何,浏览器的其他部分确实支持 UI 中渲染/交互的代理,因此将它们包含在 JS 字符串中是有意义的。
2021-04-11 09:04:52

我得出了与 bobince 相同的结论。如果要在 BMP 之外使用包含 unicode 字符的字符串,则必须重新实现 javascript 的 String 方法。这是因为 javascript 将字符计为每个 16 位代码值。BMP 之外的符号需要表示两个代码值。因此,您会遇到某些符号算作两个字符而某些仅算作一个字符的情况。

我重新实现了以下方法来将每个 unicode 代码点视为单个字符:.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;
    };
}
非常感谢您发布到公共领域。先生/女士,您是一位绅士/女士和学者。
2021-04-04 09:04:52
ucCharAt好像坏了。"🌔🌖🐺🐶🍄".ucCharAt(0)返回正确的值,但将 0 更改为 1 并返回乱码。将其更改为 2 并返回第二个(而不是第一个)符号。所以为了得到最后一个符号,你必须调用ucCharAt(8)比字符串的 ucLength 大的那个。
2021-04-06 09:04:52

最近的 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'

是的你可以。尽管根据 ECMAScript 标准直接在源文档中支持非 BMP 字符是可选的,但现代浏览器允许您使用它们。自然,必须正确声明文档编码,并且对于大多数实际目的,您需要使用 UTF-8 编码。而且,你需要一个可以处理UTF-8的编辑器,你需要一些输入法;参见例如我的完整 Unicode 输入实用程序。

使用合适的工具和设置,您可以编写var foo = '𠀁'.

非 BMP 字符将在内部表示为代理对,因此每个非 BMP 字符在字符串长度中计为 2。

使用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 "";
}