码元与码点

155 阅读2分钟

   

在日常开发项目的时候,经常会有提供用户输入的表单的情况。难免会碰到用户输入了一些奇奇怪怪的生僻字或者emoji,如果有对输入的长度有计算需求的话,往往会遇到以下的情况:

'𠮷啊'.length; // 3
'🙈啊'.length; // 3

出现上述的原因是js对字符的编码一开始使用的规范是ucs-2。ucs-2规定每一个文字ta对应16位(二进制编码)的空间,这16位的空间被称之为码元,即一个文字对应一个码元。

随着生僻字越来越多,还加入了emoji表情,16位的空间不够用了。于是js把编码方式换成了utf-16,ta允许一个文字可以占用16位空间,即一个码元。也可以占用32位(二进制编码)的空间,也就是两个码元。这样一来,这些生僻字或者emoji表情就占用了两个码元。

而js里面字符串的length属性,实际上ta数的是字符占用码元的数量。 字符串的下标,也指的是码元的下标。

如果取生僻字或者emoji表情的第一个下标,这样得到第一个码元的值输出来会是乱码,因为一个码元ta形成不了文字。

'𠮷啊'[0]; // "�"

如果想要取到想要的东西,应该使用码点来进行处理。码点可以对应16位空间,也可以对应32位空间,一个文字就是一个码点。

// slice - 通过码点取
String.prototype.sliceByPoint = function(pStart = 0, pEnd = this.length) {
	let result = ''; // 截取的结果
	let pIndex = 0; // 码点的指针
	let cIndex = 0; // 码元的指针
	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;
}

'𠮷啊🙈啊'.slice(0, 3); // "𠮷啊" '𠮷啊🙈啊'.sliceByPoint(0, 3); // '𠮷啊🙈'

// 根据码点计算字符串长度
function is32bit(char, i) {
    // 如果码点大于了16位二进制的最大值,则其是32位的
    return char.codePointAt(i) > 0xffff;
}

// 获取字符串length属性
function getStringLength(string) {
    let len = 0;
    for (let i = 0; i < str.length; i++) {
        // i在索引码元
        if (is32bit(str, i)) {
            // 当前字符串,在i这个位置,占用了两个码元
            i++;
        }
        len++;
    }
    return len;
}

getStringLength('𠮷啊🙈啊'); // 4

当然,更简便的办法是使用es6处理

// 取length属性
let str = '𠮷啊🙈啊';
str.length; // 6
[...str].length; // 4
Array.from('𠮷啊🙈啊').length; // 4

// slice
[...str].slice(0, 3); // ['𠮷', '啊', '🙈']