最近在处理一些特殊国家的语言,如果按照常规方法截取字符串长度,容易出现一种现象就是截取长度后,字符串内容发生变化了,因此记录下原因及正常处理的方法
原因
背景:早期字符是以ucs-2的格式存储,每个文字对应16位空间,每个空间就是一个码元,到了后期字符越来越多,就以utf-16格式存储,每个文字对应16或者32位,即有可能会是两个码元存储一个字符,而我们常用length去截取的话,一般都是针对码元去操作的。𠮷
是两个码元存储,a
是一个码元存储,一个字符就是一个码点,一个码点可以包含两个码元或者一个码元
字符以 UTF-16 的格式储存,每个字符固定为2
个字节。对于那些需要4
个字节储存的字符(Unicode 码点大于0xFFFF
的字符),JavaScript 会认为它们是两个字符。因此使用常规字符串段截取,如果存在大于0xFFFF
的字符就会出现问题
var s="𠮷a";
s.length // 3
s.charAt(0) // '\uD842'
s.charAt(1) // '\uDFB7'
s.charAt(2) // 'a'
s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271
s.charCodeAt(2) // 97
String.fromCharCode(s.charCodeAt(0)) //'\uD842'
String.fromCharCode(s.charCodeAt(1)) //'\uDFB7'
String.fromCharCode(s.charCodeAt(2)) //'a'
上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7
,UTF-16 编码为0xD842 0xDFB7
(十进制为55362 57271
),需要4
个字节储存。对于这种4
个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2
,而且charAt()
方法无法读取整个字符,charCodeAt()
方法只能分别返回前两个字节和后两个字节的值
识别大于0xFFFF
的字符
ES6 提供了String.fromCodePoint()
方法,可以识别大于0xFFFF的字符
ES6 提供了codePointAt()
方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点
var s = '𠮷a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97
String.fromCodePoint(s.codePointAt(0)) //'𠮷'
String.fromCodePoint(s.codePointAt(1)) //'\uDFB7'
String.fromCodePoint(s.codePointAt(2)) //'a'
codePointAt()
方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“𠮷a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的20BB7
)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt()
方法的结果与charCodeAt()
方法相同。
codePointAt()
方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt()
方法相同。
使用常规方法截取长度问题
var s = '𠮷a';
s.length // 3
s.charAt(2) // 'a'
String.fromCharCode(s.codePointAt(2))//'a'
codePointAt()
方法的参数,仍然是不正确的。比如,上面代码中,以肉眼观察字符a
在字符串s
的正确位置序号应该是 1,但是必须向codePointAt()
方法传入 2。解决这个问题的一个办法是使用for...of
循环,因为它会正确识别 32 位的 UTF-16 字符。
处理处理大于0xFFFF
的字符串
1、for...of
var s = '𠮷a';
for (let ch of s) { console.log(ch,ch.length,ch.codePointAt(0),String.fromCodePoint(ch.codePointAt(0)))
}
// 𠮷 2 134071 𠮷
// a 1 97 a
2、使用扩展运算符(...
)进行展开运算。
var s='𠮷a';
var arr = [...s];
arr.forEach(ch=>{
console.log(ch,ch.length,ch.codePointAt(0),String.fromCodePoint(ch.codePointAt(0)))
})
//𠮷 2 134071 𠮷
//a 1 97 a
3、通用长度截取方法
String.prototype.sliceByPoint = function (pStart, pEnd) {
let result = "";//最终截取结果
let pIndex = 0;//码点的指针(一个码点代表一个字符串)
let cIndex = 0;//码元的指针(超过0xffff的码点拥有两个码元)
while (1) {
if (pIndex >= pEnd || cIndex >= this.length) {
break;
}
const point = this.codePointAt(cIndex);
if (pIndex >= pStart) {
result += String.fromCodePoint(point);
}
pIndex++;
cIndex += point > 0xffff ? 2 : 1;
}
return result
}
"𠮷a".sliceByPoint(0,1) //"𠮷"
"𠮷a".sliceByPoint(1,2) //"a"
"𠮷a".slice(0,1) //"uD842"
"𠮷a".slice(1,2) //"uDFB7"
"𠮷a".slice(2,3)//"a"