前段时间我有这么一个开发需求,要求把字符串分割成一个数组,每一个数组元素放一个字符。这个需求乍一看很好实现,直接一个 .split('')
就搞定了,然而实际操作中很快就会发现一个问题:位于 Unicode 扩展区的字符,例如 emoji,会被一分为二,变成两个问号方块。
解决办法也很简单,使用 ES6 新增的展开运算符就能解决这个问题。没错,字符串也是可以被展开的。下面是一个例子:
let str = '𠮷,吉';
str.length; // 输出:4
[...str]; // 输出:['𠮷', ',', '吉']
那么为什么会出现这种问题呢?这就涉及到了一个字符编码的重要知识。
码元和码点
众所周知,每一个字符都有其对应的 Unicode 编码。起初 Unicode 编码只有 U+0000 到 U+FFFF,用 16 位二进制存储,每个字符所占用的一串编码便是码元。然而这只有 65535 个字符,这显然不太够用,于是便有了扩展区。扩展区中的字符用 32 位二进制存储,需要占用两个码元,这两个码元结合起来便是码点(又称 “码位”)。
简而言之:一个码点包含一个或两个码元。(当然在遥远的未来也许会有四个码元,但这不是现阶段的人类应该考虑的问题)
所以实际上,.split('')
只是粗暴地按照码元来切割字符,完全不管这些码元是否有个搭档。而展开运算符不一样,它会优雅地按照码点来切割字符串。
别的解决方案
如果不使用展开运算符该怎么办?
那就写个循坏呗。那么如何判断一个字符是否包含两个码元呢?我们知道 JS 的字符串中有一个 charCodeAt()
方法,可以返回字符串中指定索引位置码元的十进制编码。嗯,没错,是码元,所以我们不应该使用这个方法,而是 codePointAt()
方法,顾名思义就是返回字符串中指定索引位置码点的十进制编码(请注意这里依然是基于码元的字符索引)。
于是思路就很清晰了,我们只要写一个循环,判断当前索引位置的码点编码是否大于 0xffff
,是则使用 substring()
方法切割两个字符,并将循环索引 +2,否正切割一个字符,并将循环索引 +1。
当然实现的方法不唯一,你也可以试试 .split('')
切割为数组然后用 shift()
方法拿走元素,不过这很麻烦而且问题很多,我不推荐这么做。