Emoji 那些事儿


如今 emoji 已经成为文字交流的重要基础。离开这些精巧的符号,只怕很多对话早就因尴尬和误解而草草收场了。还记得当年短信风行时的那些事吗?

没有笑脸表情的文字聊天过程中,常常会得到“你不是在开玩笑吧?”这样的回复,以免将一些无聊的笑话信以为真。后来并没有花多久的时间,大家都明白了,单纯靠文字来理解那些幽默与调戏并不那么容易(但不管怎么说,这种套路确实应该少一些)。世界上首个 emoji 诞生之后不久,emoji 很快成为文字交流中不可或缺的要素。

日用之而不觉,我从未思考过 emoji 在技术层面上是如何工作的。但无论如何,它们肯定和 Unicode 有关系,尽管我确实不了解实际机制。老实说,我倒也没怎么在意……

读了 Wes Bos 的一条推文之后,我的想法被彻底改变。Wes Bos 在这条推文中分享了一些 JavaScript 字符串操作,其中也包括表示家庭的 family emoji。

[...'👨‍👩‍👦']   // ["👨", "‍", "👩", "‍", "👦"]

'👨‍👩‍👦'.length // 8

OK, 对字符串使用展开运算操作倒没什么稀奇的,可是一个符号拆分出了三个符号外加两个空字符,我颇有些疑惑。接着看到该符号的 length(长度) 竟然是 8,愈加困惑,展开数组中明明就只有五项啊。

当即测试这段代码,丝毫不爽,果然如 Wes 所述。什么鬼?不深入了解 Unicode、JavaScript 和 emoji,就难解我心头之惑。


Unicode 简介

JavaScript 为什么会如此处理 emoji 呢?欲要理解个中原理,还需深入去看 Unicode 本身。

Unicode 是国际计算机工业标准。它是一个字母(或字符、符号)对应一个数值的映射集。如果没有 Unicode,像那些含有像德文字母 ß、ä、ö 这样的特殊字符的文档,就无法在其他不使用这类字符的系统上共享。感谢 Unicode 的跨平台、跨系统编码。

Unicode 中共有 1,114,112 个不同的码点(code point),它们通常使用 U+ 加上一个十六进制数字表示。Unicode 码点取值范围是 U+0000 到 U+10FFFF。

这些码点总数超过十亿,它们被分为 17 个“平面”(plane)。每个平面包含六万五千多个码点。其中,最重要的平面是“多语言基本平面”(Basic Multilingual Plane,BMP),范围是 U+0000 至 U+FFFF。

BMP 基本平面几乎包含了所有现代语言中使用到的字符,以及很多其他符号。其余 16 个平面称作“补充平面”(Supplementary Planes),其中包含一些不同的案例,比如——聪明如你,可能已经猜到了——大多数 emoji 符号的定义。


emoji 是如何定义的

我们今天所知的 emoji 至少由一个 Unicode 码点所定义。可以看下 Full Emoji Data list,其中列出了所有定义的 emoji。你可能会问,Unicode 目前到底定义了多少不同的 emoji 呢?答案是“视情况而定”,这可是计算机科学中常见的答案。要回答这个问题,首先需要理解 Unicode。

如前面所述,emoji 至少由一个码点定义。这也就意味着,还有一些 emoji 是由几种不同的 emoji 和码点组合而成的。这些组合称作序列(sequence)。有了序列,就可以做一些别的事,比方说,修饰那些中性 emoji (通常用黄色皮肤展示),让它们符合你的风格。


修饰序列

犹记得当初在聊天中发现可以按自己的肤色修饰“点赞”表情的时候,我感受到了一种包容,这个表情与我之间的联系似乎变得更加紧密了。

Unicode 中有五种修饰符,用于修饰与人相关的中性 emoji。不同的修饰符会产生不同肤色效果。修饰符基于 Fitzpatrick 量表 设定,其编码范围为U+1F3FB~U+1F3FF。

下面是使用修饰符修改 emoji 肤色的示例:

// U+1F467 + U+1F3FD

👧 + 🏽

> 👧🏽

在那些支持修饰序列的操作系统中,为码点值为 U+1F467 的小女孩 emoji 添加修饰符之后,就能得一个肤色发生变化的小女孩表情。


零宽连接序列

与人相关的,可不止肤色这一种。再看看前面提到的家庭 emoji,显然并非所有家庭都是由爸爸、妈妈、儿子三者组成的。

Unicode 中包括一个中性的表示家庭的码点(U+1F46A– ‍👪),但这并非家庭真实写照。不过,还可以使用“零宽连接符”序列(Zero-Width-Joiner sequence)创建一些不同的家庭符号。

先来谈谈工作原理:Unicode 中有一个称为零宽连接符(U+200D)的码点。它就像胶水一样,将两个码点粘在一起以单个符号的形式展现。

想想要组成一个家庭的话,需要将哪些符号连在一起呢?很简单,两个大人,一个孩子。使用零宽连接符很容易就能拼出各种各样的家庭符号。

// 中性家庭

// U+1F46A

> 👪

// 零宽连接序列: 家庭 (男人, 女人, 男孩)

// U+1F468 + U+200D + U+1F469 + U+200D + U+1F466

// 👨‍ + U+200D + 👩‍ + U+200D + 👦

> ‍👨‍👩‍👦

// 零宽连接序列: 家庭 (女人, 女人, 女孩)

// U+1F469 + U+200D + U+1F469 + U+200D + U+1F467

// 👩‍ + U+200D + 👩‍ U+200D + 👧

> ‍👩‍👩‍👧

// 零宽连接序列: 家庭 (女人, 女人, 女孩, 女孩)

// U+1F469 + U+200D + U+1F469 + U+200D + U+1F467 + U+200D + U+1F467

// 👩‍ + U+200D + 👩‍ + U+200D + 👧‍ + U+200D + 👧

> ‍👩‍👩‍👧‍👧

可以查看全部的零宽连接序列,其中的类型更加多种多样,比如,带着两个女孩的父亲。不幸的是,在本文写作的时候,这些序列的支持度并不是很好。好在零宽连接序列还能优雅降级,单个码点分别独立显示。这有助于保持特殊组合符号的语义。

// 零宽连接序列: 家庭 (男人, 女孩, 女孩)

// U+1F468 + U+200D + U+1F467 + U+200D + U+1F467

// 👨‍ + U+200D + 👧 + U+200D + 👧

> ‍👨‍👧‍👧  -> 尚不支持的情况下以这种形式显示

还有很棒的一点是,上面这些原则并不是仅仅针对家庭 emoji 的。来看看著名的 David Bowie emoji(该 emoji 的真名应该是“男歌手”)。这个表情实际上也是一个零宽连接序列,由一个男士(U+1F468)、一个零宽连接符和一个耳机(U+1F3A4)组成。

可能你已经猜到了,将男人(U+1F468)替换成女人(U+1F469),结果就是一个女歌手(女版 David Bowie)。若再引入可以修改肤色的修饰符,还可能出现一个黑人女歌手。棒棒哒!

// 零宽连接序列: 女歌手

// U+1F469 + U+1F3FF + U+200D + U+1F3A4

// 👩 + 🏿 + U+200D + 🎤

> 👩🏿🎤 -> 尚不支持的情况下以这种形式显示

然而,依然不幸,目前这种序列的支持程度也并不是很好。


emoji 数量

回答 emoji 到底有多少种,得看怎么算了。是可用于展示 emoji 的不同码点的数量吗?需要计算可以展示的各种不同的 emoji 变体吗?

如果计算可展示的不同 emoji(包括所有序列、变体),总数是 2198。如果你对计算感兴趣,可以看下 unicode.org 上的完整章节。

除了“如何计算”这个问题之外,还有一个现实问题:新的 emoji 和 Unicode 字符在不断加入规范,想要记录准确的总数还是挺困难的。


JavaScript 字符串与 16 位代码单元

JavaScript 字符串的格式是 UTF-16,使用一个 16 位的代码单元表示最常见的字符。掐指一算,这意味着一个代码单元能放下六万五千多个码点(译者注:2^16=65536),几乎和 BMP 一一对应。下面使用 BMP 中的一些符号试试看

'ツ'.length  // 1 -> U+FF82

'⛷'.length // 1 -> U+26F7

'☃'.length // 1 -> U+9731

不出所料,这些字符的 length 值正好是 1。可是,如果要用到的字符不在 BMP 范围内呢?


代理对

还可以将两个 BMP 码点结合在一起,形成一个新的码点,这就是代理对(surrogate pair)。

U+D800 到 U+DBFF 之间的保留码点用于所谓的高级代理(又作 leading surrogates,主代理),U+DC00 到 U+DFFF 之间的保留码点则用于低级代理(又作 trailing surrogates,尾代理)。

这两类码点总是同时成对出现,高级代理后面跟着低级代理。然后通过特定算法对超出范围的码点进行解码。

一起来看下面的例子:

'👨'.length          // 2

'👨'.charCodeAt(0)   // 55357  -> U+D83D // 返回主代理的码点

'👨'.charCodeAt(1)   // 56424  -> U+DC68 // (译者注:这个是尾代理码点)

'👨'.codePointAt(0)  // 128104 -> U+1F468 // 返回组合在一起的代理的码点

'👨'.codePointAt(1)  // 56424  -> U+DC68

中性的男性 emoji 的码点是 U+1F468,在 JavaScript 中无法通过单个代码单元来表示。这就是为何需要使用代理对的原因,通过两个单独的代码单元组成这个表情。

分析 JavaScript 中的代码单元,有两种可能有用的方法。一个是 charCodeAt,遇上代理对的时候,该方法会分别返回每个代理的码点。另一个方法是 codePointAt,遇上主代理时会返回代理对组合的码点,遇上尾代理时则返回尾代理的码点。

看起来有点恐怖?深有同感。强烈建议仔细 MDN 上的相关文章。

再从数学方面深入看一下这个代表男性的 emoji。通过 charCodeAt 方法,我们可以检索到组成代理对的独立代码单元。

我们得到的第一个值是 55357,也就是十六进制的 D83D,这个是高级代理。得到的第二个值是 56424,即十六进制的 DC68,这是低级代理。这两个典型的代理对经过运算后便得到了 128104,映射到 emoji 就是男性符号。

// 十六进制

0x1F468 = (0xD83D - 0xD800) * 0x400 + 0xDC68 - 0xDC00 + 0x10000

// 十进制

128104 = (55357 - 55296) * 1024 + 56424 - 56320 + 65536

JavaScript 中的 length 属性与码点数量

学习了码点的相关知识,现在可以理解这让人困惑的 length 属性了。它会返回的是码点的数量,而非一开始所认为的肉眼所见符号的数量。在处理 JavaScript 字符串的时候,这让寻找 bug 变得相当麻烦。所以处理 BMP 平面之外的符号时千万要当心。


小结

再回到 Wes 最初的例子。

// 零宽连接序列: family (man, woman, boy)

// U+1F468 + U+200D + U+1F469 + U+200D + U+1F466

[...'👨‍👩‍👦']   // ["👨", "‍", "👩", "‍", "👦"]

'👨‍👩‍👦'.length // 8


// neutral family

// U+1F46A

[...'👪']   // ['👪']

'👪'.length // 2

我们在这里看到的家庭 emoji 由一个男性、一个女性、一个男孩组成。展开运算符会检查所有码点。我们所看到的空字符并非真正的空字符,而是零宽连接符。读取该 emoji 的 length 属性会得到 8,其中每个 emoji 的 length 为 2,每个零宽连接符的 length 为 1,合起来正好是 8。

我真心享受深挖 Unicode 的过程。如果你同样对这个话题感兴趣,必须向你推荐 @fakeunicode 这个 Twitter 账号。你知道吗,甚至还有关于 emoji 的 podcast 和会议 呢。我会保持关注的,了解这些每天都在使用的小符号真是有趣极了,你可能也会感兴趣的。

更多文章

一个emoji引发的思考


关注公众号【grain先森】,回复关键词 【18福利】,获取为你准备的年终福利,更多关键词玩法期待你的探索~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 深入理解Emoji(一) —— 字符集,字符集编码深入理解Emoji(二) —— 字节序和BOM Emoji字符是...
    黎清海阅读 24,032评论 5 18
  • 某一天,leader找到我说,felix啊,这里有个小需求,给我们的实名认证中的地址加入字数限制,一天时间绰绰有余...
    felix9阅读 20,355评论 0 15
  • 前言 最先接触编程的知识是在大学里面,大学里面学了一些基础的知识,c语言,java语言,单片机的汇编语言等;大学毕...
    oceanfive阅读 3,043评论 0 7
  • 在当前这个时代(比如说公元2016年),如果你并不是在维护历史遗留的文本处理代码,没有在每个地方都使用Unicod...
    纵横而乐阅读 2,750评论 3 16
  • 之前突然发现自己对字符编码还是一知半解,基本上只是听说过各种编码的名字,对它们之间的特点和区别还是不甚了解。所以这...
    L_Zephyr阅读 1,850评论 0 4