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+00E9U+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