如今 emoji 已經(jīng)成為文字交流的重要基礎(chǔ)。離開這些精巧的符號腹尖,只怕很多對話早就因?qū)擂魏驼`解而草草收場了。還記得當年短信風(fēng)行時的那些事嗎伐脖?
沒有笑臉表情的文字聊天過程中桐臊,常常會得到“你不是在開玩笑吧?”這樣的回復(fù)晓殊,以免將一些無聊的笑話信以為真断凶。后來并沒有花多久的時間,大家都明白了巫俺,單純靠文字來理解那些幽默與調(diào)戲并不那么容易(但不管怎么說认烁,這種套路確實應(yīng)該少一些)。世界上首個 emoji 誕生之后不久介汹,emoji 很快成為文字交流中不可或缺的要素却嗡。
日用之而不覺,我從未思考過 emoji 在技術(shù)層面上是如何工作的嘹承。但無論如何窗价,它們肯定和 Unicode 有關(guān)系,盡管我確實不了解實際機制叹卷。老實說撼港,我倒也沒怎么在意……
讀了 Wes Bos 的一條推文之后坪它,我的想法被徹底改變。Wes Bos 在這條推文中分享了一些 JavaScript 字符串操作帝牡,其中也包括表示家庭的 family emoji往毡。
[...'????????']?? // ["??", "?", "??", "?", "??"]
'????????'.length // 8
OK, 對字符串使用展開運算操作倒沒什么稀奇的,可是一個符號拆分出了三個符號外加兩個空字符靶溜,我頗有些疑惑开瞭。接著看到該符號的 length(長度) 竟然是 8,愈加困惑罩息,展開數(shù)組中明明就只有五項啊嗤详。
當即測試這段代碼,絲毫不爽瓷炮,果然如 Wes 所述断楷。什么鬼?不深入了解 Unicode崭别、JavaScript 和 emoji,就難解我心頭之惑恐锣。
Unicode 簡介
JavaScript 為什么會如此處理 emoji 呢茅主?欲要理解個中原理,還需深入去看 Unicode 本身土榴。
Unicode 是國際計算機工業(yè)標準诀姚。它是一個字母(或字符、符號)對應(yīng)一個數(shù)值的映射集玷禽。如果沒有 Unicode赫段,像那些含有像德文字母 ?、?矢赁、? 這樣的特殊字符的文檔糯笙,就無法在其他不使用這類字符的系統(tǒng)上共享。感謝 Unicode 的跨平臺撩银、跨系統(tǒng)編碼给涕。
Unicode 中共有 1,114,112 個不同的碼點(code point),它們通常使用 U+ 加上一個十六進制數(shù)字表示额获。Unicode 碼點取值范圍是 U+0000 到 U+10FFFF够庙。
這些碼點總數(shù)超過十億,它們被分為 17 個“平面”(plane)抄邀。每個平面包含六萬五千多個碼點耘眨。其中,最重要的平面是“多語言基本平面”(Basic Multilingual Plane境肾,BMP)剔难,范圍是 U+0000 至 U+FFFF胆屿。
BMP 基本平面幾乎包含了所有現(xiàn)代語言中使用到的字符,以及很多其他符號钥飞。其余 16 個平面稱作“補充平面”(Supplementary Planes)莺掠,其中包含一些不同的案例,比如——聰明如你读宙,可能已經(jīng)猜到了——大多數(shù) emoji 符號的定義彻秆。
emoji 是如何定義的
我們今天所知的 emoji 至少由一個 Unicode 碼點所定義〗嵴ⅲ可以看下 Full Emoji Data list唇兑,其中列出了所有定義的 emoji。你可能會問桦锄,Unicode 目前到底定義了多少不同的 emoji 呢扎附?答案是“視情況而定”,這可是計算機科學(xué)中常見的答案结耀。要回答這個問題留夜,首先需要理解 Unicode。
如前面所述图甜,emoji 至少由一個碼點定義碍粥。這也就意味著,還有一些 emoji 是由幾種不同的 emoji 和碼點組合而成的黑毅。這些組合稱作序列(sequence)嚼摩。有了序列,就可以做一些別的事矿瘦,比方說枕面,修飾那些中性 emoji (通常用黃色皮膚展示),讓它們符合你的風(fēng)格缚去。
修飾序列
猶記得當初在聊天中發(fā)現(xiàn)可以按自己的膚色修飾“點贊”表情的時候潮秘,我感受到了一種包容,這個表情與我之間的聯(lián)系似乎變得更加緊密了易结。
Unicode 中有五種修飾符唇跨,用于修飾與人相關(guān)的中性 emoji。不同的修飾符會產(chǎn)生不同膚色效果衬衬。修飾符基于 Fitzpatrick 量表 設(shè)定买猖,其編碼范圍為U+1F3FB~U+1F3FF。
下面是使用修飾符修改 emoji 膚色的示例:
// U+1F467 + U+1F3FD
?? + ??
> ????
在那些支持修飾序列的操作系統(tǒng)中滋尉,為碼點值為 U+1F467 的小女孩 emoji 添加修飾符之后玉控,就能得一個膚色發(fā)生變化的小女孩表情。
零寬連接序列
與人相關(guān)的狮惜,可不止膚色這一種高诺。再看看前面提到的家庭 emoji碌识,顯然并非所有家庭都是由爸爸、媽媽虱而、兒子三者組成的筏餐。
Unicode 中包括一個中性的表示家庭的碼點(U+1F46A– ???),但這并非家庭真實寫照牡拇。不過魁瞪,還可以使用“零寬連接符”序列(Zero-Width-Joiner sequence)創(chuàng)建一些不同的家庭符號。
先來談?wù)劰ぷ髟恚篣nicode 中有一個稱為零寬連接符(U+200D)的碼點惠呼。它就像膠水一樣导俘,將兩個碼點粘在一起以單個符號的形式展現(xiàn)。
想想要組成一個家庭的話剔蹋,需要將哪些符號連在一起呢旅薄?很簡單,兩個大人泣崩,一個孩子少梁。使用零寬連接符很容易就能拼出各種各樣的家庭符號。
// 中性家庭
// 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 + ??
> ????????????
可以查看全部的零寬連接序列矫付,其中的類型更加多種多樣凯沪,比如,帶著兩個女孩的父親技即。不幸的是,在本文寫作的時候樟遣,這些序列的支持度并不是很好而叼。好在零寬連接序列還能優(yōu)雅降級,單個碼點分別獨立顯示豹悬。這有助于保持特殊組合符號的語義葵陵。
// 零寬連接序列: 家庭 (男人, 女孩, 女孩)
// U+1F468 + U+200D + U+1F467 + U+200D + U+1F467
// ??? + U+200D + ?? + U+200D + ??
> ???????????-> 尚不支持的情況下以這種形式顯示
還有很棒的一點是,上面這些原則并不是僅僅針對家庭 emoji 的瞻佛。來看看著名的 David Bowie emoji(該 emoji 的真名應(yīng)該是“男歌手”)脱篙。這個表情實際上也是一個零寬連接序列,由一個男士(U+1F468)伤柄、一個零寬連接符和一個耳機(U+1F3A4)組成绊困。
可能你已經(jīng)猜到了,將男人(U+1F468)替換成女人(U+1F469)适刀,結(jié)果就是一個女歌手(女版 David Bowie)秤朗。若再引入可以修改膚色的修飾符,還可能出現(xiàn)一個黑人女歌手笔喉。棒棒噠取视!
// 零寬連接序列: 女歌手
// U+1F469 + U+1F3FF + U+200D + U+1F3A4
// ?? + ?? + U+200D + ??
> ?????? -> 尚不支持的情況下以這種形式顯示
然而硝皂,依然不幸,目前這種序列的支持程度也并不是很好作谭。
emoji 數(shù)量
回答 emoji 到底有多少種稽物,得看怎么算了。是可用于展示 emoji 的不同碼點的數(shù)量嗎折欠?需要計算可以展示的各種不同的 emoji 變體嗎贝或?
如果計算可展示的不同 emoji(包括所有序列、變體)怨酝,總數(shù)是 2198傀缩。如果你對計算感興趣,可以看下 unicode.org 上的完整章節(jié)农猬。
除了“如何計算”這個問題之外赡艰,還有一個現(xiàn)實問題:新的 emoji 和 Unicode 字符在不斷加入規(guī)范,想要記錄準確的總數(shù)還是挺困難的斤葱。
JavaScript 字符串與 16 位代碼單元
JavaScript 字符串的格式是 UTF-16慷垮,使用一個 16 位的代碼單元表示最常見的字符。掐指一算揍堕,這意味著一個代碼單元能放下六萬五千多個碼點(譯者注:2^16=65536)料身,幾乎和 BMP 一一對應(yīng)。下面使用 BMP 中的一些符號試試看
'?'.length??// 1 -> U+FF82
'?'.length // 1 -> U+26F7
'?'.length // 1 -> U+9731
不出所料衩茸,這些字符的 length 值正好是 1芹血。可是楞慈,如果要用到的字符不在 BMP 范圍內(nèi)呢幔烛?
代理對
還可以將兩個 BMP 碼點結(jié)合在一起,形成一個新的碼點囊蓝,這就是代理對(surrogate pair)饿悬。
U+D800 到 U+DBFF 之間的保留碼點用于所謂的高級代理(又作 leading surrogates,主代理)聚霜,U+DC00 到 U+DFFF 之間的保留碼點則用于低級代理(又作 trailing surrogates狡恬,尾代理)。
這兩類碼點總是同時成對出現(xiàn)蝎宇,高級代理后面跟著低級代理弟劲。然后通過特定算法對超出范圍的碼點進行解碼。
一起來看下面的例子:
'??'.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 上的相關(guān)文章车伞。
再從數(shù)學(xué)方面深入看一下這個代表男性的 emoji。通過 charCodeAt 方法喻喳,我們可以檢索到組成代理對的獨立代碼單元另玖。
我們得到的第一個值是 55357,也就是十六進制的 D83D表伦,這個是高級代理谦去。得到的第二個值是 56424,即十六進制的 DC68蹦哼,這是低級代理鳄哭。這兩個典型的代理對經(jīng)過運算后便得到了 128104,映射到 emoji 就是男性符號纲熏。
// 十六進制
0x1F468 = (0xD83D - 0xD800) * 0x400 + 0xDC68 - 0xDC00 + 0x10000
// 十進制
128104 = (55357 - 55296) * 1024 + 56424 - 56320 + 65536
JavaScript 中的 length 屬性與碼點數(shù)量
學(xué)習(xí)了碼點的相關(guān)知識妆丘,現(xiàn)在可以理解這讓人困惑的 length 屬性了。它會返回的是碼點的數(shù)量局劲,而非一開始所認為的肉眼所見符號的數(shù)量勺拣。在處理 JavaScript 字符串的時候,這讓尋找 bug 變得相當麻煩容握。所以處理 BMP 平面之外的符號時千萬要當心宣脉。
小結(jié)
再回到 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 賬號嫂沉。你知道嗎稽寒,甚至還有關(guān)于 emoji 的 podcast 和會議 呢。我會保持關(guān)注的趟章,了解這些每天都在使用的小符號真是有趣極了杏糙,你可能也會感興趣的。
更多文章
關(guān)注公眾號【grain先森】蚓土,回復(fù)關(guān)鍵詞 【18福利】宏侍,獲取為你準備的年終福利,更多關(guān)鍵詞玩法期待你的探索~