猜一猜
"hello".length = ? "🌝".length = ? "🇮🇳".length = ?
解答:
"hello".length = 5
"🌝".length = 2
"🇮🇳".length = 4
你答对了吗?没答对的话接着往下看吧
看一看
在日常的 JavaScript 开发中,我们习惯于使用 .length 属性来获取字符串的长度,或用 split('') 来分割字符。然而,当面对日益复杂的文本,尤其是包含 Emoji、特殊符号和多语言字符时,这些传统方法往往会给出与用户视觉感知不符的错误结果。
让我们看一看一些著名的产品是如何处理 Emoji 的:
掘金的评论区:
即梦的效果发布页:
可以看到,掘金将 🌝 当成一个字符处理,即梦将 🌝 当成两个字符处理,为什么会有这些差异呢?让我们继续探索吧。
刚刚测试了一些著名产品,让我们来看看最近很火的 AI 又是怎么处理 Emoji 的
如果你让 kimi 返回你输入的 👨👩👧👦 ,可以发现中间会闪过 👨、👩 ...,这又是为什么呢?
核心原理:理解 Unicode “字素簇” (Grapheme Cluster)
Unicode : 是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
-
码点: Unicode 给每个字符分配的一个“身份证号”,用十六进制表示,通常写作
U+XXXX的形式。- 汉字
你的码点是:U+4F60
- 汉字
-
编码方式: Unicode 有多种编码方式,常见的有 UTF - 8、UTF - 16 和 UTF - 32。
Emoji 的编码:
-
维基百科解释:
- Emoji(日语:絵文字/えもじ emoji */?),是使用在网页和聊天中的形意符号,最初是日本在无线通信中所使用的视觉情感符号(图画文字)。
-
基本 emoji:是指在 Unicode 编码表中用 1 个唯一码点表示的 emoji
-
为什么同一个 emoji 在不同设备、不同软件中显示不同?
- Unicode 只是约定了码点到 emoji 的映射关系,并没有约定 emoji 图形,每个 emoji 字体文件可以按照自己的想法设计 emoji。
MacOS/IOS的字体:Apple Color Emoji、Window的字体:Segoe UI Emoji
字素集:在 Unicode 中通常一个码点对应一个字符,但是 Unicode 引入了特定的机制允许多个 Unicode 码点组合成一个字形符号。这样由多个码点组合成的一个字符称作字素集。比如 '🤦🏼♂️'.length=7,由多个基础 emoji 和修饰符、连接符组成。
肤色修饰符:大多数人形相关的 Emoji 默认是黄色的,为 emoji 引入肤色,引入了五个新码点作为修饰符:
-
1F3FB 🏻、1F3FC 🏼、1F3FD 🏽、1F3FE 🏾、1F3FF 🏿 。
-
肤色修饰符追加到现有的 emoji 后面则形成新的变种,如:👋
U+1F44B+ 🏽U+1F3FD= 👋🏽- 👋 的 UTF-16 值是
'\uD83D\uDC4B' - 🏾 的 UTF-16 值是
'\uD83C\uDFFD' - 🏻 的 UTF-16 值是
'\uD83C\uDFFB' - 🏿 的 UTF-16 值是
'\uD83C\uDFFF'
- 👋 的 UTF-16 值是
-
5种肤色取值基于菲茨帕特里克度量
-
-
菲茨帕特里克度量(Fitzpatrick scale)是一种人类肤色的分类模式。它是由美国皮肤科医生托马斯·B·菲茨帕特里克于1975年发明的
- I 型,皮肤最白但有雀斑
- II型,肤色浅,但比I型深(在 emoji 肤色中, I 和 II 进行了合并)
- III型,肤色为橄榄色
- IV型,肤色为棕色
- V型,肤色为深棕色
- VI型,肤色最深
-
-
-
菲茨帕特里克修饰符使用示例
-
零宽度连接符(ZWJ):Unicode 通过多个基础 emoji 组合的形式表示某些复杂 emoji。组合的方式是在两个 emoji 之间添加一个U+200D,即:零宽度连接符(ZERO-WIDTH JOINER,简写为 ZWJ)
-
比如:
- 👩 + ZWJ+ 🌾 = 👩🌾
- 👩 + ZWJ+ 💻 = 👩💻
键位符:共有12个键位符,包括 #️⃣ *️⃣ 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 。
- 规则:井号 +
U+FE0F变成 emoji,再加上U+20E3变成带框框的键位符
其他连接符:变体选择符、旗帜·双字母连字、标签序列
所以讲到这里,是不是知道为什么有的 emoji 的 length 和视觉看到的不一样了呢?(因为他们可能由多个码点组成,或者可能有若干个连接符)
扩展: Emoji 网站:emojipedia.org/
- 专门用于解释和展示各种表情符号(emoji)的含义和用法
一轮带着笑脸的满月,就像月中人一样。通常将月亮描绘成一个黑色圆盘,上面画着一张略带微笑的人脸和鼻子。
另见🌕满月。也可以用来更广泛地表示月亮。通常使用笑脸。偶尔也用于讽刺和挖苦。
苹果和WhatsApp的眼睛向左看,像是斜视。三星和Facebook的表情则直视前方。谷歌的面部表情类似于😏傻笑脸,Twitter的类似于🙂微微微笑的脸。
谷歌的表情此前让人联想到莱尼脸,微软的眼睛看起来像是闭着的,Twitter的表情类似于🌚新月脸,而Facebook的表情则是一个简单的黄色笑脸。
满月脸于2010年以“带脸的满月”的名称被批准为Unicode 6.0的一部分,并于2015年添加到Emoji 1.0中。
解决方案 grapheme-splitter 与 Intl.Segmenter
grapheme-splitter 是一个轻量级 JavaScript 库,用于把任意字符串拆分成“用户眼中”的一个个视觉字符(Unicode 官方术语叫 grapheme cluster)。
在 JavaScript 里,如果直接用 split('') 或者 .length,常常会把一个“看起来是一个字符”的 emoji、带附加符号的字母或某些复杂脚本拆成多个码点,导致计数、截断、光标移动等逻辑出错。
实践提醒:涉及用户输入长度限制、光标移动、文本截断、字符串反向删除等场景,切勿直接使用 length 或 split('')。请基于“字素集(grapheme)”进行分割与计数,保证与用户视觉一致。
npm 源码分析
核心原理在于它完整实现了 Unicode UAX-29 标准 ,这是处理文本字素簇(grapheme clusters)的官方规范。
API:Intl.Segmenter
developer.mozilla.org/zh-CN/docs/…
Intl.Segmenter对象支持语言敏感的文本分割,允许你将一个字符串分割成有意义的片段(字、词、句)。- 原理:基于 Unicode UAX #29 标准,使用 ICU 库在底层进行语言敏感的文本边界检测。
- 使用方法:
function getGraphemeLength(str) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const segments = segmenter.segment(str);
return [...segments].length;
}
console.log(getGraphemeLength("🇮🇳")); // 输出:1
console.log(getGraphemeLength("中")); // 输出:1
console.log(getGraphemeLength("👦🏻")); // 输出:1
console.log(getGraphemeLength("😂")); // 输出:1
console.log(getGraphemeLength("👩👩👧👦")); // 输出:1 (家庭emoji)
console.log(getGraphemeLength("👨•👩•👧•👦")); // 输出 7
- 版本限制:要求 node 版本 > 16