ZWJ (零宽连接符)
宽度为零的连接符。在 Unicode 中用于把多个字符或表情符号结合为一个显示单元的控制字符 (U+200D)。
ZWJ (Zero Width Joiner) 是 Unicode 的控制字符 (U+200D),它不会显示在屏幕上,但具有把前后的字符结合起来、当作一个书写素簇处理的作用。最贴近日常的用途是表情符号的结合。例如家庭表情符号 👨👩👧👦 由 男性 + ZWJ + 女性 + ZWJ + 女孩 + ZWJ + 男孩 这 7 个码位构成。
用 ZWJ 结合表情符号,是一种从有限的码位造出多样表情符号的机制,并一直在逐步扩展。与职业、性别的组合 (👨🎤 = 男性 + ZWJ + 麦克风)、家庭构成、多人组成的群体等,都是用这种方式表现的。不过在不支持 ZWJ 序列的环境中,不会被结合,而是各个表情符号并排显示。
常被混淆的一点是,肤色是与 ZWJ 不同的机制。肤色是把表情符号修饰符 (U+1F3FB〜U+1F3FF) 放在人物表情符号的正后方来指定的,🙋🏽 是"举手的人"加修饰符这 2 个码位,并不包含 ZWJ。ZWJ 所承担的,是把已经修饰过的表情符号彼此束成一个显示单元的部分。
在字符计数上,ZWJ 是个麻烦的存在。看上去是 1 个表情符号,内部却由多个码位构成,因此"长度"会随数法而改变。以 👨👩👧👦 为例,JavaScript 的 .length 返回 UTF-16 代码单元数,所以是 11;Python 的 len() 按 Unicode 码位数是 7;Swift 按书写素簇数是 1。JavaScript 中 Array.from() 和展开语法也是码位单位,只会得到 7,要按人眼所见的 1 个字符来数,就需要以书写素粒度使用 Intl.Segmenter。
在社交媒体的字数限制中,X (旧 Twitter) 的数法很独特。X 采用加权长度这一方式,上限为 280,拉丁字母等的权重为 1,日语和表情符号的权重为 2 (只有日语时实质为 140 个字符)。重要的是,ZWJ 序列也被整体当作 1 个表情符号、以权重 2 来计数这一点。X 公开的字符计数一致性测试中就包含这样的例子:把家庭表情符号 👨👩👧👦 排列 140 个的字符串,其加权长度为 280,可以发布。也就是说,按码位数会成为 980 的字符串,与单纯的 140 个表情符号得到同样的对待。因此"ZWJ 序列会吃掉字数所以要避开"这种节省,在 X 上并不成立。另一方面,如果在自制的表单等处把码位数或 UTF-16 代码单元数设为上限,那么像家庭表情这样较长的 ZWJ 序列,一个就会消耗 7〜11 (随序列的组成元素数而变化)。设计上限时,需要先定好按哪个单位来数。
另一个陷阱是字符串的截断。在 ZWJ 序列的中途切断,原本是 1 个的表情符号会变成另外几个表情符号的排列,或者失去了结合对象的 ZWJ 残留在末尾。截断不按码位单位、而按书写素簇单位进行才是安全的。