"在 JavaScript 中将 Emoji 转换为 Unicode"
Emoji 不只是简单字符,许多位于 Unicode 辅助平面。理解 JavaScript 如何处理它们,对字符串长度、数据库存储和跨平台显示尤其重要。
问题:为什么 .length 会误导你?
JavaScript 字符串采用 UTF-16。A、☃(雪人)等字符位于基本多文种平面(BMP),占一个 16 位码元。😀 等 Emoji 位于辅助平面,用两个 16 位码元组成的代理对表示。
'A'.length; // 1 (BMP)
'☃'.length; // 1 (BMP)
'😀'.length; // 2 (Supplementary Plane - Surrogate Pair)
'👨👩👧👦'.length; // 11 (Wait, what? See "ZWJ" below)
方案一:String.prototype.codePointAt()
ES6 之前使用 charCodeAt(),它只返回代理对的前半部分。现代 JavaScript 的 codePointAt() 可以正确获取完整的 Unicode 码点。
const emoji = '😀';
// ❌ The old way (Incorrect for emojis)
console.log(emoji.charCodeAt(0)); // 55357 (Surrogate lead)
// ✅ The modern way
console.log(emoji.codePointAt(0)); // 128512
console.log(emoji.codePointAt(0).toString(16)); // "1f600"
方案二:String.fromCodePoint()
用静态方法 String.fromCodePoint() 将 Unicode 数值还原为可见 Emoji。
// Decimal
String.fromCodePoint(128512); // "😀"
// Hexadecimal
String.fromCodePoint(0x1F600); // "😀"
// Multiple points
String.fromCodePoint(0x1F1FA, 0x1F1F8); // "🇺🇸"
批量转换多个字符
包含多种 Emoji 和字符的字符串可以一次转换。展开运算符 [...] 按 Unicode 码点迭代,不会拆开代理对。
1. 字符串转数值(编码)
使用 .map() 获取十进制或十六进制的 Unicode 数值数组:
const text = "Hi 😀 🚀";
// Convert to Decimal numbers
const decimals = [...text].map(char => char.codePointAt(0));
console.log(decimals);
// [72, 105, 32, 128512, 32, 128640]
// Convert to Hexadecimal strings (common for CSS/JS escape)
const hexCodes = [...text].map(char => `0x${char.codePointAt(0).toString(16)}`);
console.log(hexCodes);
// ["0x48", "0x69", "0x20", "0x1f600", "0x20", "0x1f680"]
2. 数值转字符串(解码)
将数值还原为可读字符串,使用 String.fromCodePoint 配合展开语法 ...:
const myNumbers = [128512, 128640, 9731];
// This "spreads" the array items as individual arguments
const result = String.fromCodePoint(...myNumbers);
console.log(result); // "😀🚀☃"
3. 使用简单循环
如果不熟悉 .map(),也可以用 for...of 循环:
const input = "🍎🍊";
for (let char of input) {
let hex = char.codePointAt(0).toString(16);
console.log(`Character: ${char} -> Unicode: U+${hex.toUpperCase()}`);
}
// Character: 🍎 -> Unicode: U+1F34E
// Character: 🍊 -> Unicode: U+1F34A
复杂 Emoji:零宽连接符(ZWJ)
一些 Emoji 看似单个字符,实际由多个 Unicode 码点组成,以**零宽连接符(ZWJ,\u200D)**相连。
例如家庭 Emoji 👨👩👧👦 由以下序列组成:
男人 + ZWJ + 女人 + ZWJ + 女孩 + ZWJ + 男孩。
使用展开运算符 [...] 或 Array.from() 可以正确遍历其码点,但不会把整个家庭序列当成一个显示字符:
const complexEmoji = '👨👩👧👦';
// ❌ Standard split (breaks the emoji)
console.log(complexEmoji.split(''));
// ["\ud83d", "\udc68", "", "\ud83d", ...]
// ✅ Unicode-aware iteration
const points = [...complexEmoji];
console.log(points);
// ["👨", "", "👩", "", "👧", "", "👦"]
// 1. Get code points as Hexadecimal (commonly used in docs)
const hexCodes = [...complexEmoji].map(c => `0x${c.codePointAt(0).toString(16)}`);
console.log(hexCodes);
// ["0x1f468", "0x200d", "0x1f469", "0x200d", "0x1f467", "0x200d", "0x1f466"]
// 2. Get code points as Decimal (Non-Hexadecimal)
const decimalCodes = [...complexEmoji].map(c => c.codePointAt(0));
console.log(decimalCodes);
// [128104, 8205, 128105, 8205, 128103, 8205, 128102]
// 3. Convert Decimal back to Emoji
console.log(String.fromCodePoint(...decimalCodes));
// "👨👩👧👦"
// 4. Convert Hex strings back to Emoji
const emojiFromHex = String.fromCodePoint(...hexCodes);
console.log(emojiFromHex);
// "👨👩👧👦"
实际用途
- **数据库存储:**确保 MySQL 等数据库使用
utf8mb4存储这些四字节字符。 - **输入限制:**限制个人简介或推文长度时,
[...str].length可按码点计数,而str.length按码元计数。ZWJ 组合仍含多个码点,不等同于用户看到的字符数。 - **自定义文字渲染:**适用于 Canvas 游戏或高性能界面组件。
需要交互工具?
经常进行这些转换,可以试试在线 Unicode 与 Emoji 转换器,实时双向转换文字、十六进制、CSS 与 JS 转义序列。