码元与码点
关于码元和和码点,通过一个例子进行介绍。
如图,字符串'😊'只有一个“笑脸”符号,但是通过length属性发现,“长度”为2,string.length到底表示什么?
答:码元的个数
什么是码元?码元就是编码的最小单元,UTF-16和UCS-2的码元为16个比特(2字节)。也就是说,'😊'使用了两个码元,也就是4字节进行编码。
通过string.charCodeAt(index)方法可以返回对应位置的码元。
那什么是码点呢?码点就是对应字符的编码,通过对应编码规则,将编码转换为1个或多个码元。
通过string.codePointAt(index)方法可以返回对应位置的码点。
这里再说回到Unicode和UCS-2、UTF-16等之间的关系。
Unicode编码,是统一的,对于一个确定的字符,他的Unicode编码就是确定的。就拿上面的例子来说,'😊'的Unicode编码为128522。那UTF-8、UTF-16等等这些编码是什么?这里更加容易理解的说法就是,他们都是Unicode编码的一种格式,也就是不同的“转换规则”。
这里使用了一个在线的编码工具https://www.qqxiuzi.cn/bianma/Unicode-UTF.php
可以看到,同样的Unicode编码,在UTF-8和UTF-16两种编码方式下,变成了完全不同的“格式”。但是,通过对应的编码规则“逆向”的推导,最终推导出Unicode的编码是一致的。
下面来看一下UTF-16怎么对'😊'进行编码:
由于输出时,默认使用10进制输出数字,为了看清楚码元和码点的关系,可以使用16进制输出。
可以看到,从码点编码为多个码元并不是简单的“组装”。UTF-16编码规则如下:
参考https://www.cnblogs.com/malecrab/p/5300503.html
如果码点(即字符编码)值大于0xFFFF,则将其减去0x10000,之后,在该数字的前10位(bits)加上0xD800,就得到UTF-16四字节编码中的前两个字节(第一个码元);该数字的后10位(bits)加上0xDC00,得到后两个字节(第二个码元)。
就拿上面的例子说:
'😊':Unicode编码位0x1F60A
0x1F60A - 0x10000 = 0xF60A = 0x0F60A = 0000 1111 0110 0000 1010
前10bits = 00 0011 1101 = 0x03D
后10bits = 10 0000 1010 = 0x20A
前10bits + 0xD800 = 0x03D + 0xD800 = 0xD83D
后10bits + 0xDC00 = 0x20A + 0xDC00 = 0xDE0A
可以看到,通过UTF-16的转换规则,将码点为0x1F60A的字符,转换为0xD83D和0xDE0A两个码元。
码元、码点和字符串相关的方法
1. 获取指定位置的码元
String.proto.charCodeAt(index)
上面也提到过,string.length返回码元的个数,所以这里的index有效范围是[0, length - 1]
2. 获取指定位置的字符(如果可以用一个码元进行编码,码元的值和码点的值相等)
String.proto.charAt(index)
charCodeAt()一个返回的是码元(数值),而charAt()返回码元对应的字符,如果一个字符有多个码元,这里仅仅取出其中一个码元,并把它当作码点,就会出现乱码。
3. 将字符串指定位置的码元转换成码点(如果是多个码元表示的码点,从指定位置开始,取多个码点进行转换)
String.proto.codePointAt(index)
如图,“笑脸”码元开始的位置是1;当传入2的时候,下标2位置对应的是“笑脸”的第二个码元,不是完整的两个码元开始的位置,所以直接返回了第2个码元,而不是转换成码点返回。
4.通过码元创建字符串
String.proto.charCodeFrom(...charCode)
将码元转换成字符串
5.通过码点创建字符串
String.proto.codePointFrom(...codePoint)
根据上面的解释我们可以得到最新的字符串截取函数
const str = '你好😊😂🤣😍哈哈';
//在String的原型上加pointLength用于判断长度
String.prototype.pointLength = function(){
let len = 0;
for (let i = 0; i < this.length;) {
const codePoint = this.codePointAt(i);
i += codePoint > 0xffff ? 2 : 1;
len++;
}
return len;
}
//在String的原型上加pointAt用于获取指定下标内容
String.prototype.pointAt=function(index){
let curIndex = 0;//码点的下标
for (let i = 0; i < this.length;) {
const codePoint = this.codePointAt(i);
if (curIndex === index) {
return String.fromCodePoint(codePoint);
}
i += codePoint > 0xffff ? 2 : 1;
curIndex++;
}
}
//在String的原型上加sliceByPoint用于截取字符串
String.prototype.sliceByPoint = function(start=0,end=this.pointLength()){
let res = '';
for (let i = start; i < end; i++) {
res += this.pointAt(i);
}
return res;
}
console.log(str.pointLength());//8
console.log(str.pointAt(3));//😊
console.log(str.sliceByPoint(2,4));//😊😂