Matrix 首页推荐

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。

文章代表作者个人观点,少数派仅对标题和排版略作修改。


本文尝试讲解 Emoji 的一些有趣的知识,来帮助我们更好的理解这个全世界通用的语言。在开启之前,先看看你的 Emoji 考试能有几分:

  1. 你知道不同的肤色 👍👍🏻👍🏼👍🏽 是怎么实现的吗
  2. 你知道为什么早期 iOS 总会被 Emoji 的问题弄出 crash 吗
  3. 你知道为什么会有两种 Emoji 吗?🅰︎ 🅰️1

从下面开始👇🏻,为了最好的理解📖,可以打开你的浏览器🌍,跟我写点 JavaScript🧑🏻‍💻,一起看看 Emoji 的世界🚗。

定义

这里我们先统一 Emoji 的定义,到底哪些东西算 Emoji?

  • iPhone 输入法里点击 😃 按钮
  • 微信、抖音的表情
  • Telegram 在输入 Emoji 的时候,会有表情包联想。发送 Emoji 后还会有动画

第一个场景是 Emoji 肯定是毋庸置疑的。它们虽然像图片,但是你复制粘贴后就是能随便发。

而其他都不算。这里我们做一个定义:只有能被操作系统定义的编码才有可能是 Emoji。

怎么理解这段话?

微信的表情你在输入后,当你退出到列表界面,或者尝试复制它,只能得到类似 [微笑] 的字符,而不是我们看到的微笑表情。所以它们本质上文本,只是微信对它做了图片替换,并保证在删除表情的时候也能直接删除 4 个字符,从而让表现上看上去就和文字类似。

相信你也遇见过这个场景,朋友发了一个消息,你在通知上看到的是一个 😊,你正奇怪这个朋友从来不发 emoji 的时候,点进去一看其实就是微信的 [微笑] 表情。这里为什么会这样呢?

原因是通知中心的文字里只能是纯文本,所以微信不能对实际上是 [微笑] 的字符做替换,就采用了对这种字符做 Emoji 替换的方式,让其在通知中心的表现不会像有 bug。但需要吐槽的是,微信的微笑应该和 🙂 这个映射才对~

Telegram 是个非常优秀的社交软件,称他是 IM 的天花板完全不为过。所以你在 Telegram 中发 Emoji 可能会得到一个它们进行定制的动画,特别有喜感。但你将 Emoji 发出去后,它被传播的形式并不是 Emoji,而是一个带有动画的图片。只不过这个图片你在复制的时候,得到的还是 Emoji 本身。

Telegram 对 Emoji 做的动画

综上我们看到的结论为:Emoji 就是文字,它能被复制粘贴,只是看上去是个彩色图片,也只有文字才能被编码。而文字和图片最重要的区别就是字节数量,最大的 Emoji 被编码出来后不会超过 30 个字节,而图片往往要几百字节起。那 Emoji 为普通文字的区别又是什么?

Emoji 在每个操作系统里都会有一个专门的字体去定义,比如 Apple 就是 Apple Color Emoji。

Image.png

大家熟知的粗体,斜体等文字的风格调整往往都是由字体文件去做定义,它们描述了文字被加粗后的样子。如果字体文件缺失了这个信息的话,操作系统可能回尝试去模拟,也可能就直接忽略。所以这意味着:Emoji 无法被加粗😂

我们在划定好 Emoji 的范围后才能进一步讨论,接下来先看看它的起源。

起源

E(え)mo(も)ji(じ)—絵文字,其中 e 是 绘,moji 代表文字的意思,最早起源于日本的 20 世纪 90 年代,由软银推出。

softbank-1997-emoji-set-apple-comparison-emojipedia.jpeg

接着的事情大家都知道了,iPhone 面世,为了占领日本市场(我有一亿人需要用 Emoji 呢),就合作将其弄了进去。并随着 iPhone 的热度提升,Emoji 也开始世界范围内大火 。接着 Emoji 被纳入 Unicode,大部分 Emoji 都有了自己的码位,成为一个事实上的标准。这样消费者也不用担心 iPhone 手机发的 Emoji 安卓看不到了。

但在当时那个年代,大部分手机屏幕并不是彩色的,所以一开始很多 Emoji 其实是这个样子的:

🅰︎🅱︎,它们和 🅰️🅱️ 又有什么区别?

NOTE:在实际测试的时候我发现少数派在展示这种 Emoji 的时候,在 Safari 和 Chrome 上表现也有差别,可能会有展示问题。

我们以 🅰︎ 为例子。在最开始的时候,🅰︎ 的 Unicode 编码应该是 1f170但之后屏幕开始变成彩色,于是定义一套彩色的字符开始有必要。但直接将文字形式的 Emoji 替换成彩色形式可能会对之前的表达带来误解,所以为了兼容这种旧形式,采取了一种比较「奇怪」的方式来获得图片形式——Variation Selector。

Unicode 中定义了很多不同版本的 Variation Selector,我们只要记住对应 Text 的版本是 15,对应 Emoji 的版本是 16,后文将分别简写为 VS-15(0xfe0e),VS-16(0xfe0f)。于是🅰︎和🅰️的实际编码分别是 0x1f170 0xfe0e 和 0x1f170 0xfe0f。

如果你感兴趣的话,可以打开浏览器的控制台,输入看看效果。

String.fromCodePoint(0x1f170, 0xfe0e)
String.fromCodePoint(0x1f171, 0xfe0f)

所以后续的 Emoji 输入法中,都会刻意添加 VS 来做区分。但之前这些没加的文本应该怎么处理?

Unicode 建议针对这些没有 VS 符号的字符,采用 VS-15 作为默认,意味着 1f170 应该渲染成文字形式。但是 Apple 可能是为了秀自己的 Emoji 字体,并没有遵守这个标准。所以在不同平台上会有不同效果,比如这个 🈹️

Image.png
Image.png

如果你用过飞书的话,可能就体验过系统输入法给你的建议是图片形式的 Emoji,但是发送出去后就变成文本模式了。就是因为输入法建议的 Emoji 丢了 VS,所以两边的软件具有不同的解释方式

旗帜问题

接着根据这个 VS-16,让我们来看看 iOS 10 时期出现的一个 bug

简单点讲就是一旦收到某人发送过来的含有”🏳️‍0🌈”的短消息,将会导致设备死机,需要重启手机才能解决。

想要解释清楚这个 bug,先要了解一下旗帜的规则。

我们先记住一个原则,Unicode 非常抠门,为了节省码位它们什么事情都干得出来。你想想,世界上,那么多国家。国家内部可能还有一些地区,这些地区也有自己的旗帜。如果一个个编码显得过于奢侈和复杂。举个例子,假设 Unicode 先给国旗放 500 个码位,然后现有国家用掉了 300 个。未来又有一些奇怪的旗帜也想进去凑数,但这样可能导致剩下的 200 个也不够放,那又需要临时开一个区域放新的码位。针对这个问题,Unicode 也提出了一种解决方案,对气质做了分类

  1. 最简单的旗帜:🚩🏳️🏁🏴🎌。这些旗帜都有独立的码位,不过为了兼容早期,可能会跟着一个 VS-16。
  2. 国旗—🇨🇳🇺🇸🇬🇧。
  3. 地区旗帜—🏴󠁧󠁢󠁥󠁮󠁧󠁿🏴󠁧󠁢󠁳󠁣󠁴󠁿。
  4. 特殊旗帜—🏳️‍🌈🏴‍☠️

国旗

它们是由专门的 Regional Indicator Symbol(RIS) 组合而来,从 A 到 Z,码位分别是从 1f1e6 到 1f200,光是两两组合就有 26 * 26 中方案。打开控制台输入

Array.from({ length: 26 }, (_, i) => String.fromCodePoint(0x1f1e6 + i)).join(',')

就能看到所有的字符。为什么要用 join(',') 而不是 join('')?试试看😏

所以🇨🇳就是 🇨 🇳 放在一起,同样的🇺🇸的就是 🇺 🇸。

如果你对这种两个字符放在一起,会变成新的字符的特性不是很熟悉的话,可能会认为这是 Emoji 的特性,但其实这种被称为 Combining Character 的技术很早就已经被支持。

最常见的使用场景就是拉丁语和音调符号的组合。比如 é 有一个单独的码位 e9,但是它也可以由 e 和 ´ 组合,它们的码位分别是 65 和 301。所以如果你在浏览器输入 '\u{65}\u{301}' 也能得到 é。但是像 JS 这种相对落后的语言并不会认为 '\u{65}\u{301} === \u{e9}'true,反观 Swift 这种相对现代的语言,则对这个特性有了良好的支持。

地区旗帜

我不太懂政治,不太清楚像英格兰和苏格兰这两个地方不是国家(世界杯都是两支队伍),但是它们都有自己的旗帜。这里的组合逻辑就很复杂了,首先需要一个 🏴 跟上 ISO 定义的 RIS,再跟上一个 Cancel Tag(007f)*。像这里英格兰的 RIS 是 *GBENG,可能是 Great Britain EnGLand 的缩写。换句话说,一个🏴󠁧󠁢󠁥󠁮󠁧󠁿就占了 7 个码位,比直接写「英格兰」浪费多了🌚

彩虹旗

这些旗帜本质上是通过 Emoji 组合起来的。比如彩虹旗就是 🏳️+ZWJ+🌈,🏴‍☠️是🏴+ZWJ+☠️。ZWJ(Zero Width Joiner) 可以理解成 Emoji 胶水,用来粘合一些用于特殊意义的 Emoji,以达到节省码位的目的,它们被称之为 Emoji ZWJ Sequence,毕竟谁能知道 10 年后,我们手机里有多少个 Emoji 呢?插一句题外话,当你发现项目中 if (result == '') 分支怎么都不为真的时候,可能就是有人在代码里下毒了。

所以,回到我们上面提到 iOS 10 曾经的一个 bug,表面上是🏳️0🌈这种组合,如果直接输入这个几个符号并不会有什么问题,因为它们的对应的编码是 1f3f3, fe0f, 30, 1f308。而会让机器的 Crash 的信息对应的编码 1f3f3, fe0f, 200d, 30, 1f308—其中多出来的 200d 就是 ZWJ。所以说,造成这个漏洞的原因可能是:1f3f3, f30f, 200d 同时出现后,系统预期 200d 后面的字符可以组合一个新的 Emoji。但是通过人为添加的 0,过于想当然的系统就 Crash 了。

接着我们继续展开 ZWJ,看看有哪些 Emoji 是基于这个来创造的。

Emoji ZWJ Sequence

这个的使用场景除了上面提到的物体之间的组合之外,更多的还是人之间的组合。比如👨和特殊的物品组合在一起,变成了某个专业人士。👨和👩组合在一起,👨👩🧒在一起,👨👨 在一起,都能组合家庭。还有一些虚幻的人物,比如🧚‍♀️,🧟‍♀️这些,从 Emoji 12 开始,出于政治正确的目的,都将这些虚幻人物默认设置为「中性」,并通过 ZWJ 拼接 ♂️和 ♁️。最让我觉得有趣的还是爱情相关的符号:👩‍❤️‍💋‍👩 👩‍❤️‍👨。

下面我们来详细讨论这些

专业人员

👩‍🎤👩‍🏫👩‍🔧👩‍🎓,这四个 Emoji 对应的分别是女歌手、女教师、白皮肤的女技师、女大学生。根据 Emoji 的规则就是:

👩‍🎤:👩 + ZWJ + 🎤

👩‍🏫:👩 + ZWJ + 🏫

👩‍🔧:👩 + ZWJ + 🔧

👩‍🎓:👩 + ZWJ + 🎓

注意到基本公式就是 性别 Emoji + ZWJ + 一个能表示该专业人员的典型物体。虽然在学校的女人不一定仅仅是老师,还可能是家长、学生,但是一个老师由「女人+ 学校」组成我们很容易记忆。

其实通过这个规律我们可以看到很有意思的效果——

'👩' + '\u200d' + '🎤' // 👩‍🎤
'👩‍🎤'.replace('🎤', '🎓') // 👩‍🎓

引用 Twitter 上的一幅图:

Photo Apr 16, 2020 at 104117 PM.jpeg

虚构人物

我尝试尽量列出所有虚拟人物:

  • 超级英雄:🦸🦸‍♂️🦸‍♀️
  • 超级反派:🦹🦹‍♂️🦹‍♀️
  • 法师:🧙🧙‍♂️🧙‍♀️
  • 仙子:🧚 🧚‍♂️🧚‍♀️
  • 吸血鬼:🧛🧛‍♂️🧛‍♀️
  • 人鱼:🧜🧜‍♂️🧜‍♀️
  • 精灵:🧝🧝‍♂️🧝‍♀️
  • 妖怪:🧞🧞‍♂️🧞‍♀️
  • 僵尸:🧟🧟‍♂️🧟‍♀️

这些虚拟人物的 Emoji 饱含了深深的政治正确。首先,任何角色都有一个男性,也有一个女性在里面,特别是美人鱼还有男性这个完全忍不了,甚至在 iOS 15.4 中还有怀孕的男性 Emoji 出现 :)

但是呢,Emoji 标准还更进一步,它们又多了一个没有性别的符号,这样的改动看上去似乎是因为近几年平权运动的兴起,所以多了一个符号。

但是 Emoji 的组合公式也变了,相比之前女性角色是由 男性角色加上 ♂️ 符号,这种过于男权的表现。现在改为一个没有性别的 Emoji + ZWJ + ♂️或者 ♀️ 表示对应的性别组合。

[...'🧜‍♂️'].map(a => a.codePointAt(0).toString(16))
// ["1f9dc", "200d", "2642", "fe0f"] (4)

爱情

一开始对爱情的定义应该挺容易的:💑,男性和女性之间出现了个❤️。公式为 👨 + ZWJ + ❤️ + ZWJ + 👩。秉承着包容的态度,Emoji 也对爱情的定义进行了扩容—👨‍❤️‍👨👩‍❤️‍👩

有了爱情的下一步就是 kiss,所以 👨‍❤️‍💋‍👨 👩‍❤️‍💋‍👩 也自然少不了。Kiss 相比爱情的 Emoji,是在❤️ 后面接个ZWJ 和 💋‍‍‍——Interesting。

有了爱情之后,自然就会组成家庭👪

Family

家庭最开始也挺简单的,爸爸👨和妈妈👩加上几个孩子的组合。一个👦,一个👧,一👦一👧,两个👧,两个👦,就五种——👨‍👩‍👦 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 。

公式为 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦,你可以很容易验证。

[...'👨👩👧👦'].join('\u{200d}')

但是不知道从什么时候开始,家庭的概念也被扩充了。有单亲家庭,有同性家庭:👨‍👨‍👧‍👦👩‍👩‍👦‍👦👨‍👧。不过公式相比早期的家庭,仅仅是将 👨 👩 删去一个,或者换成两个同样 Emoji,比如 [...'👨👧👦'].join('\u{200d}')

当然,少不了无性别的符号:👪

非标准

比如微软对 🐈 的拓展,🐱‍👤 🐱‍💻。这些符号应该只能在 Windows 系统上看到效果,它们的样子类似于图片所示

 

皮肤和发型

我们现在已经知道 Emoji 是如何通过组合不同的字符来得到新的 Emoji,而这里的魔力就是 ZWJ。上面提到的规则就涵盖了我们在 Emoji 键盘中绝大多数 Emoji 的组成规则。接着,我们再来看看长按 Emoji 键盘的表现。图片中我以一个卷发男人的 Emoji 为例,长按后可以得到另外的 5 种颜色。

IMG_2217.jpeg

我记得在几年前,iOS 刚刚支持展示这些皮肤的时候,大家认为默认的颜色代表了「黄种人」,有点点自豪感,但是似乎又过于黄了—这些老外是不是没看过我们亚洲人啊?之所以将基准颜色设计成这种黄色,仅仅是因为世界上没有任何人长这样。而 Emoji 中的部分颜色已经具有含义了,比如绿色的代表了中毒🤢,红色代表了中暑🥵,蓝色代表冷死🥶,所以黄色显然是一个挺不错的选择。

这五种颜色又是基于什么来判定的?有一种 Fitzpatrick(菲茨帕特里克) Scale 分类法,就被 Emoji 皮肤系统采用,虽然最初是为了评估不同肤色的人对紫外线的反应程度。只不过相比 Fitzpatrick Scale 的 6 种的分类,Emoji 仅仅引入了 5 种,它将第一种和第二种合并了,因为这样会看上去更真实。

640px-Influence_of_pigmentation_on_skin_cancer_risk.png

所以在 Emoji 中表示颜色的方式就是人物或者人物器官,加上肤色的 Emoji。它们分别是

🏻 🏼 🏽 🏾 🏿,码位对应:1f3fb, 1f3fc, 1f3fd, 1f3fe, 1f3ff。

需要强调的是,肤色和下面将要提到的发型,在和支持的 Emoji 组合在一起的时候,是不需要 ZWJ 的,所以 💪 + 🏻 就是 💪🏻。

可能是工作量的问题,家庭,爱情这些 Emoji 还没有来得及支持肤色。反而是👫的 Emoji 最先支持了多种肤色的排列组合。

接着,除了皮肤之外,还有发型。红发 🦰卷发🦱 秃头🦲以及白发🦳,它们的码位分别是 1f9b0, 1f9b1, 1f9b2, 1f9b3。我不太清楚为什么会选用这四种发型。

和皮肤一样,满足公式的 Emoji 并排放在一起就能看到组合成新的的 Emoji —白发的女性 👩‍🦳。因为皮肤和发型并不能直接通过 Emoji 键盘获得,所以如果需要的话,可以通过运行下面的代码:

Array.from({ length: 5 }, (_, i) => String.fromCodePoint(i + 0x1f3fb))
Array.from({ length: 4 }, (_, i) => String.fromCodePoint(i + 0x1f9b0))

总结

相信看到这里,大家应该已经了解了 Emoji 的相关原理,也能对有些时候编辑 Emoji 的一些怪异行为坦然以对。比如下面肤色的符号: 🏻,看上去👈有一个多出来的空格,但其实你就是删不掉 😊

如果又因为新的 bug 导致了 iOS 崩溃也不要嘲笑,因为这真的很难 :)

CleanShot 2022-02-12 at 14.38.21.gif
在 Safari 上,英格兰要删好多次才能删除干净

参考资料

> 下载 少数派 2.0 客户端、关注 少数派公众号,解锁全新阅读体验 📰

> 实用、好用的 正版软件,少数派为你呈现 🚀