跳转到内容

码元和码点

js
const str = '😊👌🙌🤣🥱';

console.log(str.length); // 10
console.log(str[0]); // �
console.log(str.slice(1, 3)); // ��
console.log(str.slice(0, 2)); // 😊

在上面的代码中,str 是一个由 emoji 组成的字符串,你会发现它的长度是 10,而不是 5,而输出第一个字符是一个乱码。

为什么呢?

这就得提到了码元码点了。

在 js 中,字符是以 UTF-16 的格式存储的,也就是说,一个字符是 2 字节(0 ~ 65535, 0x0000 ~ 0xffff),而一个码元就是 2 字节。但是 0 ~ 65535 之间的码元并不能表示所有的字符,像 emoji 一般都是两个码元,而一个字符真正占用的空间就是码点。

知道这个概念后,我们知道一个码点对一个字符,但是一个码元不一定对应一个字符。所以,在 js 中,字符串的 length 属性返回的是码元的个数,而不是码点的个数。

而输出字符串的第一个字符,实际上是输出它的码点,所以输出的是乱码。第三个 log 是则是输出第一个字符的第二个码点和第二个字符第一个码点,所以是乱码。第四个 log 则是输出第一个字符的第一个码点和第二个字符的第一个码点,所以是正确的。

codePointAt 方法

codePointAt 方法接收一个索引作为参数,返回该索引的码点的值。

js
const str = '😊';

console.log(str.codePointAt(0)); // 122852

通过 codePointAt 方法返回值的大小,可以一个字符是否为两个码元组成。从而判断是否为 emoji 等。

js
const str = '😊a';

console.log(str.codePointAt(0) > 0xffff); // true
console.log(str.codePointAt(2) > 0xffff); // false

注意:codePointAt 如果取的是码点的第二部分,以上判断字符是否为两个码元组成的方式将会不准

js
const str = '😊';

console.log(str.codePointAt(1)); // 56842

fromCodePoint 方法

fromCodePoint 方法接收一个或多个码点值作为参数,返回对应的字符串。

js
console.log(String.fromCodePoint(128522)); // 😊

console.log(String.fromCodePoint(128522, 97)); // 😊a

自定义方法

获取字符串码点数量

通过 length 属性获取字符串长度,是不准的,字符串码点数量才是正确的字符串长度。

js
/**
 * 获取字符串码点数量
 * @param {String} str
 */
function pointLength(str) {
	let len = 0;
	for (let i = 0; i < str.length; ) {
		len++;
		i += str.codePointAt(i) > 0xffff ? 2 : 1;
	}
	return len;
}

const str = '😊👌🙌🤣🥱';

console.log(str.length); // 10
console.log(pointLength(str)); // 5

获取对应码点索引的字符

js
/**
 * 获取对应码点索引的字符
 * @param {string} str
 * @param {number} index 码点的索引
 * @returns {string | undefined}
 */
function pointAt(str, index) {
	let pointIdex = 0;
	for (let i = 0; i < str.length; ) {
		if (pointIdex === index) {
			return String.fromCodePoint(str.codePointAt(i));
		}
		pointIdex++;
		i += str.codePointAt(i) > 0xffff ? 2 : 1;
	}
}

const str = '😊👌🙌🤣🥱';

console.log(pointAt(str, 0)); // 😊
console.log(pointAt(str, 2)); // 🙌
console.log(pointAt(str, 4)); // 🥱

基于码点的截取

js
/**
 * 基于码点的截取
 * @param {string} str
 * @param {number} start 码点起始索引
 * @param {number} end 码点结束索引(不包含结束索引的值)
 * @returns {string}
 */
function pointSlice(str, start, end) {
	let result = '';
	let pointIdex = 0;
	for (let i = 0; i < str.length; ) {
		if (pointIdex >= start && pointIdex < end) {
			result += String.fromCodePoint(str.codePointAt(i));
		}
		pointIdex++;
		i += str.codePointAt(i) > 0xffff ? 2 : 1;
	}
	return result;
}

const str = '😊👌🙌🤣🥱';

console.log(pointSlice(str, 0, 1)); // 😊
console.log(pointSlice(str, 2, 5)); // 🙌🤣🥱

退格

如果简单的使用 str.slice(0, -1),是不准的,如下。

js
const str = '😊👌🙌🤣🥱';
console.log(str.slice(0, -1)); // 😊👌🙌🤣�

因为 slice 是基于码元的

js
/**
 * 退格
 * @param {string} str
 * @returns {string}
 */
function backspace(str) {
	if (str.length > 1) {
		const pos = str.length - 2;
		const len = str.codePointAt(pos) > 0xffff ? 2 : 1;
		return str.slice(0, str.length - len);
	} else {
		return '';
	}
}

console.log(backspace('a')); // ''
console.log(backspace('abc')); // ab
console.log(backspace('abc😊')); // abc
console.log(backspace('😊a')); // 😊
console.log(backspace('😊👌🙌🤣🥱')); // 😊👌🙌🤣

更简单的

js
/**
 * 退格
 * @param {string} str
 * @returns {string}
 */
function backspace(str) {
	return Array.from(str).slice(0, -1).join('');
}

console.log(backspace('a')); // ''
console.log(backspace('abc')); // ab
console.log(backspace('abc😊')); // abc
console.log(backspace('😊a')); // 😊
console.log(backspace('😊👌🙌🤣🥱')); // 😊👌🙌🤣

以上方法也可以使用 Array.from 替代。