Emoji.prototype.length —— Unicode 字元那些事兒

發表於2017-05-09

譯者注:本文用到了很多 emoji 符號,建議不要使用 Windows 系統閱讀本文。

如今 emoji 已經成為文字交流的重要基礎。離開這些精巧的符號,只怕很多對話早就因尷尬和誤解而草草收場了。還記得當年簡訊風行時的那些事嗎?

沒有笑臉表情的文字聊天過程中,常常會得到“你不是在開玩笑吧?”這樣的回覆,以免將一些無聊的笑話信以為真。後來並沒有花多久的時間,大家都明白了,單純靠文字來理解那些幽默與調戲並不那麼容易(但不管怎麼說,這種套路確實應該少一些)。世界上首個 emoji 誕生之後不久,emoji 很快成為文字交流中不可或缺的要素。

日用之而不覺,我從未思考過 emoji 在技術層面上是如何工作的。但無論如何,它們肯定和 Unicode 有關係,儘管我確實不瞭解實際機制。老實說,我倒也沒怎麼在意……

讀了 Wes Bos 的一條推文之後,我的想法被徹底改變。Wes Bos 在這條推文中分享了一些 JavaScript 字串操作,其中也包括表示家庭的 family emoji。

OK, 對字串使用展開運算操作倒沒什麼稀奇的,可是一個符號拆分出了三個符號外加兩個空字元,我頗有些疑惑。接著看到該符號的 length(長度) 竟然是 8,愈加困惑,展開陣列中明明就只有五項啊。

當即測試這段程式碼,絲毫不爽,果然如 Wes 所述。什麼鬼?不深入瞭解 Unicode、JavaScript 和 emoji,就難解我心頭之惑。

Unicode 簡介

JavaScript 為什麼會如此處理 emoji 呢?欲要理解箇中原理,還需深入去看 Unicode 本身。

Unicode 是國際計算機工業標準。它是一個字母(或字元、符號)對應一個數值的對映集。如果沒有 Unicode,像那些含有像德文字母 ß、ä、ö 這樣的特殊字元的文件,就無法在其他不使用這類字元的系統上共享。感謝 Unicode 的跨平臺、跨系統編碼。

Unicode 中共有 1,114,112 個不同的碼點(code point),它們通常使用 U+ 加上一個十六進位制數字表示。Unicode 碼點取值範圍是 U+0000U+10FFFF

這些碼點總數超過十億,它們被分為 17 個“平面”(plane)。每個平面包含六萬五千多個碼點。其中,最重要的平面是“多語言基本平面”(Basic Multilingual Plane,BMP),範圍是 U+0000U+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 的小女孩 emoji 新增修飾符之後,就能得一個膚色發生變化的小女孩表情。

零寬連線序列

與人相關的,可不止膚色這一種。再看看前面提到的家庭 emoji,顯然並非所有家庭都是由爸爸、媽媽、兒子三者組成的。

Unicode 中包括一箇中性的表示家庭的碼點(U+1F46A– ‍?),但這並非家庭真實寫照。不過,還可以使用“零寬連線符”序列(Zero-Width-Joiner sequence)建立一些不同的家庭符號。

先來談談工作原理:Unicode 中有一個稱為零寬連線符(U+200D)的碼點。它就像膠水一樣,將兩個碼點粘在一起以單個符號的形式展現。

想想要組成一個家庭的話,需要將哪些符號連在一起呢?很簡單,兩個大人,一個孩子。使用零寬連線符很容易就能拼出各種各樣的家庭符號。

可以檢視全部的零寬連線序列,其中的型別更加多種多樣,比如,帶著兩個女孩的父親。不幸的是,在本文寫作的時候,這些序列的支援度並不是很好。好在零寬連線序列還能優雅降級,單個碼點分別獨立顯示。這有助於保持特殊組合符號的語義。

還有很棒的一點是,上面這些原則並不是僅僅針對家庭 emoji 的。來看看著名的 David Bowie emoji(該 emoji 的真名應該是“男歌手”)。這個表情實際上也是一個零寬連線序列,由一個男士(U+1F468)、一個零寬連線符和一個耳機(U+1F3A4)組成。

Davide Bowie Emoji

可能你已經猜到了,將男人(U+1F468)替換成女人(U+1F469),結果就是一個女歌手(女版 David Bowie)。若再引入可以修改膚色的修飾符,還可能出現一個黑人女歌手。棒棒噠!

然而,依然不幸,目前這種序列的支援程度也並不是很好。

emoji 數量

回答 emoji 到底有多少種,得看怎麼算了。是可用於展示 emoji 的不同碼點的數量嗎?需要計算可以展示的各種不同的 emoji 變體嗎?

如果計算可展示的不同 emoji(包括所有序列、變體),總數是 2198。如果你對計算感興趣,可以看下 unicode.org 上的完整章節

除了“如何計算”這個問題之外,還有一個現實問題:新的 emoji 和 Unicode 字元在不斷加入規範,想要記錄準確的總數還是挺困難的。

JavaScript 字串與 16 位程式碼單元

JavaScript 字串的格式是 UTF-16,使用一個 16 位的程式碼單元表示最常見的字元。掐指一算,這意味著一個程式碼單元能放下六萬五千多個碼點(譯者注:2^16=65536),幾乎和 BMP 一一對應。下面使用 BMP 中的一些符號試試看:

不出所料,這些字元的 length 值正好是 1。可是,如果要用到的字元不在 BMP 範圍內呢?

代理對

還可以將兩個 BMP 碼點結合在一起,形成一個新的碼點,這就是代理對(surrogate pair)。

U+D800U+DBFF 之間的保留碼點用於所謂的高階代理(又作 leading surrogates,主代理),U+DC00U+DFFF 之間的保留碼點則用於低階代理(又作 trailing surrogates,尾代理)。

這兩類碼點總是同時成對出現,高階代理後面跟著低階代理。然後通過特定演算法對超出範圍的碼點進行解碼。

一起來看下面的例子:

中性的男性 emoji 的碼點是 U+1F468,在 JavaScript 中無法通過單個程式碼單元來表示。這就是為何需要使用代理對的原因,通過兩個單獨的程式碼單元組成這個表情。

分析 JavaScript 中的程式碼單元,有兩種可能有用的方法。一個是 charCodeAt,遇上代理對的時候,該方法會分別返回每個代理的碼點。另一個方法是 codePointAt,遇上主代理時會返回代理對組合的碼點,遇上尾代理時則返回尾代理的碼點。

看起來有點恐怖?深有同感。強烈建議仔細 MDN 上的相關文章。

再從數學方面深入看一下這個代表男性的 emoji。通過 charCodeAt 方法,我們可以檢索到組成代理對的獨立程式碼單元。

我們得到的第一個值是 55357,也就是十六進位制的 D83D,這個是高階代理。得到的第二個值是 56424,即十六進位制的 DC68,這是低階代理。這兩個典型的代理對經過運算後便得到了 128104,對映到 emoji 就是男性符號。

JavaScript 中的 length 屬性與碼點數量

學習了碼點的相關知識,現在可以理解這讓人困惑的 length 屬性了。它會返回的是碼點的數量,而非一開始所認為的肉眼所見符號的數量。在處理 JavaScript 字串的時候,這讓尋找 bug 變得相當麻煩。所以處理 BMP 平面之外的符號時千萬要當心。

小結

再回到 Wes 最初的例子。

我們在這裡看到的家庭 emoji 由一個男性、一個女性、一個男孩組成。展開運算子會檢查所有碼點。我們所看到的空字元並非真正的空字元,而是零寬連線符。讀取該 emoji 的 length 屬性會得到 8,其中每個 emoji 的 length 為 2,每個零寬連線符的 length 為 1,合起來正好是 8。

我真心享受深挖 Unicode 的過程。如果你同樣對這個話題感興趣,必須向你推薦 @fakeunicode 這個 Twitter 賬號。你知道嗎,甚至還有關於 emoji 的 podcast會議 呢。我會保持關注的,瞭解這些每天都在使用的小符號真是有趣極了,你可能也會感興趣的。

相關文章