Number.MAX_SAFE_INTEGER
= 9007199254740991 = 2^53 - 1 ≈ 9PBU+????
, 其中 ????
是使用十六进制表示的值U+0000
- U+FFFF
看一个例子
const a = 'a';
a.length; // 1
const dog = '🐶';
dog.length; // 2
同样是输入了一个字符而已,但是输出的长度并不相同。从上面知道,我们看到的字符,实际上在计算机中都存储为一个 Unicode 的码点;而当字符需要加载到内存时,又会变为另一种字节序列,这需要根据字符的编码规则而决定
因此,先查一下这两个字符的 Unicode 编码是什么
对于 a
U+0061
, 在基本字符集中0x0061
对于 🐶
(查询关键词为 dog face)
U+1F436
, 超出了基本字符集0xD83D 0xDC36
可以看到,a
占用了一个字节,而 🐶
占用了两个字节,而我们知道,一个普通的字符占用的字节就是 1, 因此 a.length
为 1, 而 dog.length
为 2
额外,在 JavaScript 中,我们也可以通过不同的方法来创建字符
console.log('🐶'); // 🐶 使用字面值创建
console.log('\u{1F436}'); // 🐶 \u{xxxx} 使用 Unicode 码点创建
console.log('\uD83D\uDC36'); // 🐶 \uXXXX\uXXXX 使用十六进制的值进行创建,且编码为 UTF-16
当我们试图通过对字符串 🐶
进行长度统计时,'🐶'.length
给出的结果是 2
, 但是这从人类的角度来说,是不对的,应该是 1
这是由于在统计长度时,计算的基础是 UTF-16 编码,而 🐶
占用了两个字节,所以返回了 2, 因此我们需要将 🐶
转成 Unicode 码点后进行计算,即将字符串分拆为字符数组
const dog = '🐶';
dog.length; // 2
[...dog].length; // 1
const dogs = '🐶🐶🐶';
dogs.length; // 6
[...dogs].length; // 3
除了上面的情况,还有一些外国文字,如 é
, 它的显示,实际上还可以通过合成字符来进行的
对于 é
, 有两种方式表示
U+00E9
U+0065
+ U+0301
, 即 e
+ ´
可以在 Node 中查看一下输出
console.log('\u00E9'); // é
console.log('\u0065\u0301'); // é
'\u00E9' === '\u0065\u0301' // false
'\u00E9'.length // 1
'\u0065\u0301'.length // 2
可以看出,输出字形相同的字符,使用单一码点和使用合成码点进行比较时,却不相等,这很可能会导致想不到的 bug
那问题来了
实际上,有一个解决方案,字符串范式转换。字符串范式(Canonical form)有四种:
\u0065\u0301
, 处理时合成为 \u00E9
\u00E9
, 处理时,拆分为 \u0065\u0301
对于 JavaScript, ES6 中提供了方法 String.prototype.normalize([form])
来进行字符串的归一化操作,参数 form
默认为 NFC
const longOne = '\u0065\u0301';
const shortOne = '\u00E9';
longOne === shortOne; // false
shortOne === longOne.normalize('NFC'); // true
longOne === shortOne.normalize('NFD'); // true
longOne.normalize('NFC').length; // 1
shortOne.normalize('NFD').length; // 2