【问题标题】:Why does code points between U+D800 and U+DBFF generate one-length string in ECMAScript 6?为什么 U+D800 和 U+DBFF 之间的代码点在 ECMAScript 6 中生成单长度字符串?
【发布时间】:2017-02-11 20:49:21
【问题描述】:

我太糊涂了。为什么在使用 ECMAScript 6 原生 Unicode 帮助程序时,从 U+D800 到 U+DBFF 的代码点编码为单个(2 字节)字符串元素?

我不是在问 JavaScript/ECMAScript 如何对字符串进行原生编码,我是在问一个使用 UCS-2 编码 UTF-16 的额外功能。

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 之间的一个数字,我不确定,我只知道它有助于更​​改结果代码点。对我来说,返回值没有意义,它代表没有线索的领先。我是不是理解错了?

【问题讨论】:

  • 使用codePointAt 而不是charCodeAt。后者只会返回代理对的第一个代码单元的信息。
  • @4castle 我以 charCodeAt() 为例说明发生了什么,您可以看到这两种方法的结果字符串仅包含一个代码单元。
  • 我不确定我是否完全理解您的问题。它产生一个长度为一长度的字符串,因为这是你要求它做的。您想了解代理对吗?
  • stackoverflow.com/q/6885879/5217142 的可能重复项 - Javascript 未将字符串实现为 Unicode 字符。相反,它记录了用于对 Unicode 字符进行编码的 16 位值序列。不幸的是,这导致单个 Unicode 字符的字符串长度为 2,需要 UTF-16 编码中的代理对。
  • @handoncloud ...好吧,这不是“独特”的意思。你想要“单身”。

标签: javascript unicode ecmascript-6 utf-16


【解决方案1】:

我认为您的困惑在于 Unicode 编码的一般工作原理,所以让我试着解释一下。

Unicode 本身只是以特定的顺序指定一个字符列表,称为“代码点”。它没有告诉你如何将它们转换为位,它只是给它们一个 0 到 1114111 之间的数字(十六进制,0x10FFFF)。这些从 U+0 到 U+10FFFF 的数字有几种不同的方式可以表示为位。

在早期版本中,预计 0 到 65535 (0xFFFF) 的范围就足够了。这可以自然地用 16 位表示,使用与无符号整数相同的约定。这是存储 Unicode 的原始方式,现在称为 UCS-2。要存储单个代码点,您需要保留 16 位内存。

后来决定这个范围不够大;这意味着存在高于 65535 的代码点,您无法在 16 位内存中表示这些代码点。 UTF-16 was invented as a clever way of storing these higher code points. 它的工作原理是“如果您查看 16 位内存,并且它是介于 0xD800 和 0xDBF 之间的数字(“低代理”),那么您还需要查看接下来的 16 位内存”。任何执行此额外检查的代码都将其数据处理为 UTF-16,而不是 UCS-2。

重要的是要了解内存本身并不“知道”它使用哪种编码,UCS-2 和 UTF-16 之间的区别在于您如何解释该内存。当您编写一个软件时,您必须选择要使用的解释。

现在,到 Javascript...

Javascript 通过将字符串的内部表示解释为 UTF-16 来处理字符串的输入和输出。太好了,这意味着您可以输入并显示著名的?字符,它无法存储在一块 16 位内存中。

问题在于,大多数内置字符串函数实际上将数据处理为 UCS-2 - 也就是说,它们一次查看 16 位,而不关心它们看到的是不是一个特殊的“代理” . function you used, charCodeAt() 就是一个例子:它从内存中读取 16 位,并将它们作为 0 到 65535 之间的数字提供给您。如果您输入它?,它只会返回前 16 位;询问它之后的下一个“字符”,它会给你第二个 16 位(这将是一个“高代理”,介于 0xDC00 和 0xDFFF 之间)。

在 ECMAScript 6 (2015) 中,new function was added: 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 之外的任何长度。

这个数字唯一不寻常的是,在 Unicode 中,该值是保留 - 没有,也永远不会是字符 U+D800。那是因为它是告诉 UTF-16 算法“这只是半个字符”的神奇数字之一。因此,可能的行为是,任何创建此字符串的尝试都只是一个错误 - 例如opening a pair of brackets that you never close,它是不平衡的、不完整的。

最终得到长度为 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));

【讨论】:

  • 我喜欢这个答案,但它仍然没有回答我的问题。我已经知道 String#codePointAt() 方法,包括 String.fromCodePoint(),但它们与实际问题无关。无论如何,这是一个很酷的答案。问题在于 ES6 如何使用 UCS-2 编码我想要的代码点。
  • 是的,我知道。我知道,请再次阅读问题,您将了解实际问题。问题是浏览器的 String.fromCodePoint()\u{...} 正在编码没有代理对的代码点。请看问题
  • 我添加了 TL;DR 描述
  • 是的,从 U+D800 到 U+DFFF 的所有 Unicode 码位都是保留的,永远不会被分配意义。没有可以编码 U+D800 的前导和尾随组合;代理编码的任何数字都将高于0x010000。您可以在解码它们的公式中看到:它添加了一个固定的0x10000,因为其目的是对不适合 16 位的值进行编码。 UTF-16 基本上是 16 位编码和 32 位编码之间的丑陋折衷,可以表示2^21 - 2048 可能的代码点;这是一个巧妙的技巧,让我们陷入了这样的尴尬境地。
  • UTF-16 的存在导致 Unicode 作为一个整体来保留这些代码点。 Unicode 现在不可能为任何这些代码点赋予意义,因为它会破坏 UTF-16。 UTF-16 和 UTF-8 也以不同的方式效率低下:它们占用的内存位比理论上要多。是的,对于拉丁字符串,UTF-8 将使用比 UTF-16 更少的位;对于其他一些字符串,它将使用 more 位。从理论上讲,您可以发明一种内存布局,其中每个 Unicode 代码点占用 21 位;不过,与它一起工作会很糟糕!
【解决方案2】:

嗯,它这样做是因为规范说它必须:

这两者一起表示,如果参数是 &lt; 0&gt; 0x10FFFF,则抛出 RangeError,否则任何代码点 &lt;= 65535 将按原样合并到结果字符串中。

至于为什么要这样指定,我不知道。似乎 JavaScript 并不真正支持 Unicode,只支持 UCS-2。

Unicode.org 对此事有以下看法:

  • http://www.unicode.org/faq/utf_bom.html#utf16-2

    问:什么是代理?

    答:代理是来自两个特殊 Unicode 值范围的代码点,保留用作 UTF-16 中成对代码单元的前导和尾随值。前导,也称为高,代理从 D80016 到 DBFF16,尾随或低,代理从 DC0016 到 DFFF16。它们被称为代理,因为它们不直接表示字符,而只是作为一对。

  • http://www.unicode.org/faq/utf_bom.html#utf16-7

    问:是否存在无效的 16 位值?

    答:未配对的代理在 UTF 中无效。这些包括 D80016 到 DBFF16 范围内的任何值,后跟 DC0016 到 DFFF16,或 DC0016 到 DFFF16 范围内的任何值,前面没有 D80016 到 DBFF16.

因此,String.fromCodePoint 的结果并不总是有效的 UTF-16,因为它可以发出不成对的代理。

【讨论】:

  • 那么我问的是与错误或提案明确相关吗?
  • “好像 JavaScript 并不真正支持 Unicode,只支持 UCS-2。”,我不能同意。 JavaScript 可以支持 UTF-16,使用 UCS-2 非常好(UCS-2 中的前导 => 2 个代码单元),UTF-8 可以从Uint8Array 获得。
  • @handoncloud 这就像说 C“支持”Unicode,因为它使用 1 字节字符,可用于形成有效的 UTF-8 序列。
  • UTF-16 不是 UTF-8。正如我所说,再次............ UCS-2 非常适合形成 UTF-16,因为它使用 2 字节字符。
  • @handoncloud UTF-8 不是 UTF-16。正如我所说,八位字节非常适合形成 UTF-8,因为它使用 1 字节字符。
猜你喜欢
  • 2017-03-04
  • 2018-02-18
  • 1970-01-01
  • 2017-10-20
  • 2015-06-13
  • 2017-02-12
  • 2011-09-07
  • 2017-08-03
  • 2019-06-22
相关资源
最近更新 更多