JavaScript emoji utils
栏目: JavaScript · 发布时间: 6年前
内容简介:JavaScript的字符串处理貌似不难,直到遇上了emoji:??发生了什么?得从Unicode编码说起……
写在前面
JavaScript的字符串处理貌似不难,直到遇上了emoji:
javascript-emoji-issues
??发生了什么? 到底怎么回事?
得从Unicode编码说起……
一.Unicode编码
The Unicode codepoint range goes from U+0000 to U+10FFFF which is over 1 million symbols, and these are divided into groups called planes. Each plane is about 65000 characters (16^4). The first plane is the Basic Multilingual Plane (U+0000 through U+FFFF) and contains all the common symbols we use everyday and then some. The rest of the planes require more than 4 hexadecimal digits and are called supplementary planes or astral planes.
也就是说,Unicode支持的编码范围是 U+0000
到 U+10FFFF
,能对应100多万个符号( 0x10FFFF === 1114111
)。这些符号被分组归入16个 平面(panel) ,所以每个平面放65536( 16^4 === 65536
)个
其中,常用符号都放在第一个平面( U+0000
到 U+FFFF
)里,所以称之为 基本多语言平面(Basic Multilingual Plane,也简称BMP) ,其余的平面中的 码位值(codepoint,即符号对应的Unicode编码值) 都大于4位(16进制),称为 辅助平面(supplementary plane)
P.S.辅助平面还有个看起来 很厉害 的名字,叫astral plane(星界?星界位面?)
I have no idea if there’s a good reason for the name “astral plane.” Sometimes, I think people come up with these names just to add excitement to their lives.
此外,基本多语言平面里 65536
个位置的入住率并不是 100%
, 专门空出来一些位置 以备不时之需,比如新增特殊含义符号,或者扩展
比如UTF-16中 代理对儿(surrogate pairs) 的概念,即用两个4位(16进制)的小码位值表示一个大码位值(大于4位),算是一种从基本多语言平面到辅助平面的映射,之所以能这样做,就是因为:
基本多语言平面内,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。
二.JavaScript中的Unicode
JS中的Unicode字符有3种表示方法:
'A' === '\u0041' === '\x41' === '\u{41}'
其中 \x
仅用于 U+0000
到 U+00FF
, \u
适用于任意Unicode字符( U+0000
到 U+10FFFF
),但大于4位(大于 U+FFFF
)的话,就要用花括号( {}
)把十六进制序列包起来:
The \x can be used for most (but not all) of the Basic Multilingual Plane, specifically U+0000 to U+00FF. The \u can be used for any Unicode characters. The curly braces are required if there are more than 4 hexadecimal digits and optional otherwise.
注意, \u{}
转义语法是在ES 2015中定义的,称之为 UnicodeEscapeSequence 。之前用两个小Unicode来表示一个大Unicode,例如:
'' === '\u{1F4A9}' '' === '\uD83D\uDCA9'
\uD83D\uDCA9
就是代理对儿,形如 <H,L>
,二者的转换关系如下:
let C, L, H; C = 0x1F4A9; // 公式:大Unicode转代理对儿 H = Math.floor((C - 0x10000) / 0x400) + 0xD800; L = (C - 0x10000) % 0x400 + 0xDC00; [H, L].map(v => '\\u' + v.toString(16).toUpperCase()).join('') "\uD83D\uDCA9"
另外,JS中认为一个16位无符号整数值是一个字符,所以一个emoji可能会被认为是多个字符:
The phrase code unit and the word character will be used to refer to a 16-bit unsigned value used to represent a single 16-bit unit of text. Unicode character only refers to entities represented by single Unicode scalar values: the components of a combining character sequence are still individual “Unicode characters”, even though a user might think of the whole sequence as a single character.
P.S.关于JavaScript的Unicode支持以及ES规范的相关内容,见 JavaScript’s internal character encoding: UCS-2 or UTF-16?
正则表达式中的Unicode
既然大Unicode(大于 U+FFFF
的)在JS中用两个小Unicode(代理对儿)来表示,那么自然会写出这样的正则表达式:
> /[\uD83D\uDCA9-\uD83D\uDE0A]/.test('') Uncaught SyntaxError: Invalid regular expression: /[\uD83D\uDCA9-\uD83D\uDE0A]/: Range out of order in character class
报错无法识别这样的range,那 怎样用正则表达式描述大Unicode字符范围 呢?
JS提供了 u flag 来解决这个问题:
u Unicode; treat pattern as a sequence of Unicode code points
/[\uD83D\uDCA9-\uD83D\uDE0A]/u.test('') /[-]/u.test('')
类似的, .
(点号匹配任意字符)想要匹配代理对儿形式的大Unicode的话,也需要开启u flag:
> /foo.bar/.test('foobar') false > /foo.bar/u.test('foobar') true
P.S. /./u
仅能匹配代理对儿形式的emoji ,其它形式的不行,例如:
> /foo.bar/u.test('foo2⃣️bar') false
P.S.更多相关示例,见 Astral ranges in character classes
fromCodePoint与fromCharCode
String.fromCodePoint
与 String.fromCharCode
的区别在于,前者支持更大范围的16进制Unicode编码,例如:
> String.fromCodePoint(0x1F4A9) "" > String.fromCharCode(0x1F4A9) ""
但 fromCodePoint
由ES 2015规范定义,兼容性不如 fromCharCode
好,对于 0x0000-0xFFFF
范围的65536个Unicode字符,建议使用 fromCharCode
三.emoji编码
类似于Unicode,emoji也是一种编码规则,也有对应的规范,还存在很多个版本:
Emoji 12.0 Emoji 11.0 Emoji 5.0 Emoji 4.0 Emoji 3.0 Emoji 2.0 Emoji 1.0
其中 12.0
计划2019年才发布,最新的 11.0 发布于 2018-02-07
像HTML、CSS规范一样,新版规范中新增的emoji不一定都被实现了,并且面临的 兼容性问题比HTML、CSS更恶劣 :
-
规范版本:emoji规范发版频繁,多版本共存
-
平台差异:除了Web浏览器环境外,emoji还依赖平台原生支持(各种屏幕显示设备)
-
依赖Unicode:emoji是在Unicode基础上建立的,依赖Unicode规范
比如从短信复制粘贴到网页输入框,emoji可能就显示不出来或者乱码了,因为native与Web浏览器支持的emoji规范版本或实现程度存在差异。另外,Unicode新规范可能会与已定义的emoji规范有冲突,这时候自然得由emoji规范让步:
Unicode 12.0 is the new version of the Unicode Standard planned for release in March 2019. See Emoji 12.0 for a more complete list of potential emojis for 2019. Note: All emojis listed throughout 2018 are candidates only, and subject to change before a final release.
emoji面临的环境有多恶劣呢?如图:
emoji-unicode-platform
回到emoji规范本身,长这样子:
1F600 ; emoji ; L1 ; secondary ; x # V6.1 () GRINNING FACE 1F48F ; emoji ; L1 ; none ; j # V6.0 () KISS
最左边是Unicode码位值,被成功录入Unicode规范的话, U+1F48F
就会对应 KISS
表情:
> '\u{1F48F}' ""
除了这种与Unicode一一对应的emoji,加入Unicode大家庭外,还 有几种特殊的emoji :
-
variation selector-16 :一个不可见字符(
U+FE0F
),表示在它前面的字符应该用emoji显示 -
zero width joiner :零宽连接符,是一个零宽空格(
U+200D
),用来把多个emoji合成为一个emoji -
tone modifier:肤色修饰,一种语法,能改变前一个emoji的肤色,语法格式是
<emoji>\ud83c[\udffb-\udfff]
,即U+D83C
后面跟不同的几个值表示不同的肤色控制 -
keycap:键帽符号,键帽样式的
0-9
、#
和*
,以U+20E3
结尾 -
unofficial emoji flag:存在一些非常规国旗emoji,以黑色旗子(
U+1F3F4
)开头, 取消符号 (U+E007F
)结尾
例如:
// \ufe0f让黑心字符显示成emoji,连续两个也没关系 ':heart:️' === '\u2764\ufe0f\ufe0f' '\u2764\ufe0f' === ':heart:' '\u2764' === '❤' // 零宽连接符\u200d合成复杂表情, + :heart: + + = :heart: ':heart:' == '\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69' // 肤色修饰,黑baby、白baby '' === '\ud83d\udc76\ud83c\udfff' '' === '\ud83d\udc76\ud83c\udffb' // 键帽样式 ':hash:' === '\u0023\ufe0f\u20e3' // 非官方国旗 '' ==='\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f'
四.JavaScript里的emoji
那么 在JS里,一个emoji到底含有几个Unicode字符?
> ':golf:'.length 1 > ''.length 2 > ':one:'.length 3 > ''.length 4 > ''.length 11 > ''.length 14
一个emoji字面量的长度从 1
到 14
(还可能存在更长的)各不相同……所以,会出现这种情况:
> '我们是一家人'.slice(0, 1) "�" > '我们是一家人'.substr(0, 2) ""
期望通过 slice(0, 1)
截取第一个emoji,却 得到了一个无法显示的字符 ,甚至 substr(0, 2)
从一家4口中拆出了Man()…… 这可咋整?
对于某些emoji,有一种非常简单的处理方式, Array.from
:
> Array.from('').length 1
字符串转数组时会保持代理对儿在一起,所以 length
正确了,但 这种方法不是万能的 :
> Array.from('').length 7
P.S.类似的,支持Unicode编码转换的 bestiejs/punycode.js 也存在类似的问题:
> punycode.ucs2.decode(' ').length 1 > punycode.ucs2.decode('').length 7
也就是说, 单靠JS对Unicode的原生支持,无法正确处理含emoji的字符串 。那么,在一些场景会遇到问题:
-
表单检验字数限制
-
截取文章摘要
-
反转字符串
-
逐字符处理
-
正则匹配
-
……其它含emoji的文本处理场景
例如:
> '一个打十个'.length >= 10 === true true > '你好hi233..。'.substr(0, 10) "你好hi233�" > Array.from(':one:23').reverse().join('') "32⃣️1" > '开心'[0] === '' false > /a.b/.test('ab') false
P.S.关于JavaScript中Unicode的更多问题,见 JavaScript has a Unicode problem
五.解决方案:emoji-utils.js
要解决上面列出的一排问题,只能想办法识别emoji了,目前( 2018/09/15
)貌似还没有这样的 工具 库
手搓一个,类似于词法分析,逐字符匹配,挑出符合emoji编码规则的Unicode组合,具体见下面源码
Github地址: https://github.com/ayqy/emoji-utils
在线Demo(测试case): https://ayqy.github.io/emoji/index.html
API
提供了6个简单API:
// 是不是一个emoji isEmoji(str) // 是否包含emoji containsEmoji(str) // 字符串转Unicode数组 str2unicodeArray(str) // 计算长度 length(str) // 子串截取 substr(str = '', start = 0, len = Infinity) // 字符串转数组,相当于split('') toArray(str)
内部未暴露的方法有:
// 尝试匹配开头的emoji,失败返回'' matchOneEmoji(str, matched = '')
缺陷
但是,这些工具函数 并不100%靠谱 ,因为:
Not all browsers, UIs, etc even render :heart: as a single symbol. The code assumes the joiners are used between characters appropriately which could be very problematic.
所以, emoji-utils.js
的 实现基于3点假设 :
-
所有代理对儿都是emoji(事实上,有些代理对儿不是emoji)
-
肤色控制对所有emoji都是有效的,并且只对emoji生效(对普通文本符号无效)
-
joiner连接起来的emoji都算一个,无论显示上能否被合成一个emoji
对于第一点假设,代理对儿形式的不一定是emoji,也可能是纯文本,例如:
'\ud835\udc00' === ''
后两点假设也会导致一些badcase,例如(Chrome Console环境):
// 尝试制造黑色笑脸,未遂 '\ud83d\ude0a\ud83c\udfff' === '' // 尝试人工合成新物种,失败 '\u0023\ufe0f\u20e3\u200d\ud83d\ude0a' === ':hash:' '\ud83d\ude0a\u200d\ud83d\ude0a' === ''
这些case都会被识别成1个emoji,而Chrome Console环境显示是2个,因为它们:
-
符合emoji编码的语法规则
-
但不一定是合法的emoji
-
即便合法,当前平台也不一定支持
emoji-utils.js
假设满足第一点的就是一个独立显示的合法emoji,未考虑emoji规范版本以及平台支持性,所以存在这样的badcase。badcase可能带来的影响是:
-
isEmoji/containsEmoji()
误判类似于”的文本字符 -
length()
小于实际显示的字符长度 -
substr()/toArray()
与实际预期不符
所以能这个工具库所能识别出的字符集是emoji的超集,多出来一部分代理对儿形式的文本,以及符合emoji编码规则但在emoji规范中未定义的字符序列。尽管如此,它是 目前最健全的JS emoji处理工具 ,实际应用中足够应对大多数场景了
参考资料
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
学习JavaScript数据结构与算法(第2版)
[巴西] Loiane Groner / 邓 钢、孙晓博、吴 双、陈 迪、袁 源 / 人民邮电出版社 / 2017-9 / 49.00元
本书首先介绍了JavaScript 语言的基础知识以及ES6 和ES7 中引入的新功能,接下来讨论了数组、栈、队列、链表、集合、字典、散列表、树、图等数据结构,之后探讨了各种排序和搜索算法,包括冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序、顺序搜索、二分搜索,然后介绍了动态规划和贪心算法等常用的高级算法以及函数式编程,最后还介绍了如何计算算法的复杂度。一起来看看 《学习JavaScript数据结构与算法(第2版)》 这本书的介绍吧!