前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript emoji utils

JavaScript emoji utils

作者头像
ayqy贾杰
发布2019-06-12 14:54:39
2K0
发布2019-06-12 14:54:39
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

写在前面

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+0000U+10FFFF,能对应100多万个符号(0x10FFFF === 1114111)。这些符号被分组归入16个平面(panel),所以每个平面放65536(16^4 === 65536)个

其中,常用符号都放在第一个平面(U+0000U+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种表示方法:

代码语言:javascript
复制
'A' === '\u0041' === '\x41' === '\u{41}'

其中\x仅用于U+0000U+00FF\u适用于任意Unicode字符(U+0000U+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,例如:

代码语言:javascript
复制
'' === '\u{1F4A9}'
'' === '\uD83D\uDCA9'

\uD83D\uDCA9就是代理对儿,形如<H,L>,二者的转换关系如下:

代码语言:javascript
复制
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(代理对儿)来表示,那么自然会写出这样的正则表达式:

代码语言:javascript
复制
> /[\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

代码语言:javascript
复制
/[\uD83D\uDCA9-\uD83D\uDE0A]/u.test('')
/[-]/u.test('')

类似的,.(点号匹配任意字符)想要匹配代理对儿形式的大Unicode的话,也需要开启u flag:

代码语言:javascript
复制
> /foo.bar/.test('foobar')
false
> /foo.bar/u.test('foobar')
true

P.S././u仅能匹配代理对儿形式的emoji,其它形式的不行,例如:

代码语言:javascript
复制
> /foo.bar/u.test('foo2⃣️bar')
false

P.S.更多相关示例,见Astral ranges in character classes

fromCodePoint与fromCharCode

String.fromCodePointString.fromCharCode的区别在于,前者支持更大范围的16进制Unicode编码,例如:

代码语言:javascript
复制
> String.fromCodePoint(0x1F4A9)
""
> String.fromCharCode(0x1F4A9)
""

fromCodePoint由ES 2015规范定义,兼容性不如fromCharCode好,对于0x0000-0xFFFF范围的65536个Unicode字符,建议使用fromCharCode

三.emoji编码

类似于Unicode,emoji也是一种编码规则,也有对应的规范,还存在很多个版本:

代码语言:javascript
复制
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规范本身,长这样子:

代码语言:javascript
复制
1F600 ; emoji ; L1 ;    secondary ; x   # V6.1 () GRINNING FACE
1F48F ; emoji ; L1 ;    none ;  j   # V6.0 () KISS

最左边是Unicode码位值,被成功录入Unicode规范的话,U+1F48F就会对应KISS表情:

代码语言:javascript
复制
> '\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)结尾

例如:

代码语言:javascript
复制
// \ufe0f让黑心字符显示成emoji,连续两个也没关系
'❤️️' === '\u2764\ufe0f\ufe0f'
'\u2764\ufe0f' === '❤️'
'\u2764' === '❤'
// 零宽连接符\u200d合成复杂表情, + ❤️ +  +  = ‍❤️‍‍
'‍❤️‍‍' == '\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69'
// 肤色修饰,黑baby、白baby
'' === '\ud83d\udc76\ud83c\udfff'
'' === '\ud83d\udc76\ud83c\udffb'
// 键帽样式
'#️⃣' === '\u0023\ufe0f\u20e3'
// 非官方国旗
'' ==='\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f'

四.JavaScript里的emoji

那么在JS里,一个emoji到底含有几个Unicode字符?

代码语言:javascript
复制
> '⛳'.length
1
> ''.length
2
> '1️⃣'.length
3
> ''.length
4
> '‍‍‍'.length
11
> ''.length
14

一个emoji字面量的长度从114(还可能存在更长的)各不相同……所以,会出现这种情况:

代码语言:javascript
复制
> '‍‍‍我们是一家人'.slice(0, 1)
"�"
> '‍‍‍我们是一家人'.substr(0, 2)
""

期望通过slice(0, 1)截取第一个emoji,却得到了一个无法显示的字符,甚至substr(0, 2)从一家4口中拆出了Man()……这可咋整?

对于某些emoji,有一种非常简单的处理方式,Array.from

代码语言:javascript
复制
> Array.from('').length
1

字符串转数组时会保持代理对儿在一起,所以length正确了,但这种方法不是万能的

代码语言:javascript
复制
> Array.from('‍‍‍').length
7

P.S.类似的,支持Unicode编码转换的bestiejs/punycode.js 也存在类似的问题:

代码语言:javascript
复制
> punycode.ucs2.decode(' ').length
1
> punycode.ucs2.decode('‍‍‍').length
7

也就是说,单靠JS对Unicode的原生支持,无法正确处理含emoji的字符串。那么,在一些场景会遇到问题:

  • 表单检验字数限制
  • 截取文章摘要
  • 反转字符串
  • 逐字符处理
  • 正则匹配
  • ……其它含emoji的文本处理场景

例如:

代码语言:javascript
复制
> '‍‍‍一个打十个'.length >= 10 === true
true
> '你好hi233..。'.substr(0, 10)
"你好hi233�"
> Array.from('1️⃣23').reverse().join('')
"32⃣️1"
> '开心'[0] === ''
false
> /a.b/.test('ab')
false

P.S.关于JavaScript中Unicode的更多问题,见JavaScript has a Unicode problem

五.解决方案:emoutils.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:

代码语言:javascript
复制
// 是不是一个emoji
isEmoji(str)
// 是否包含emoji
containsEmoji(str)
// 字符串转Unicode数组
str2unicodeArray(str)
// 计算长度
length(str)
// 子串截取
substr(str = '', start = 0, len = Infinity)
// 字符串转数组,相当于split('')
toArray(str)

内部未暴露的方法有:

代码语言:javascript
复制
// 尝试匹配开头的emoji,失败返回''
matchOneEmoji(str, matched = '')

缺陷

但是,这些工具函数并不100%靠谱,因为:

Not all browsers, UIs, etc even render ‍❤️‍‍ as a single symbol. The code assumes the joiners are used between characters appropriately which could be very problematic.

所以,emoutils.js实现基于3点假设

  • 所有代理对儿都是emoji(事实上,有些代理对儿不是emoji)
  • 肤色控制对所有emoji都是有效的,并且只对emoji生效(对普通文本符号无效)
  • joiner连接起来的emoji都算一个,无论显示上能否被合成一个emoji

对于第一点假设,代理对儿形式的不一定是emoji,也可能是纯文本,例如:

代码语言:javascript
复制
'\ud835\udc00' === ''

后两点假设也会导致一些badcase,例如(Chrome Console环境):

代码语言:javascript
复制
// 尝试制造黑色笑脸,未遂
'\ud83d\ude0a\ud83c\udfff' === ''
// 尝试人工合成新物种,失败
'\u0023\ufe0f\u20e3\u200d\ud83d\ude0a' === '#️⃣‍'
'\ud83d\ude0a\u200d\ud83d\ude0a' === '‍'

这些case都会被识别成1个emoji,而Chrome Console环境显示是2个,因为它们:

  • 符合emoji编码的语法规则
  • 但不一定是合法的emoji
  • 即便合法,当前平台也不一定支持

emoutils.js假设满足第一点的就是一个独立显示的合法emoji,未考虑emoji规范版本以及平台支持性,所以存在这样的badcase。badcase可能带来的影响是:

  • isEmoji/containsEmoji()误判类似于”的文本字符
  • length()小于实际显示的字符长度
  • substr()/toArray()与实际预期不符

所以能这个工具库所能识别出的字符集是emoji的超集,多出来一部分代理对儿形式的文本,以及符合emoji编码规则但在emoji规范中未定义的字符序列。尽管如此,实际应用中足够应对大多数场景了

P.S.对于需要准确处理emoji的场景,可以考虑emoji-regex

参考资料

  • Finally moving past “”.length === 2
  • “”.length === 2
  • Can you use String.fromCodePoint just like String.fromCharCode
  • Unicode详解
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 一.Unicode编码
  • 二.JavaScript中的Unicode
    • 正则表达式中的Unicode
      • fromCodePoint与fromCharCode
      • 三.emoji编码
      • 四.JavaScript里的emoji
      • 五.解决方案:emoutils.js
        • API
          • 缺陷
            • 参考资料
            相关产品与服务
            短信
            腾讯云短信(Short Message Service,SMS)可为广大企业级用户提供稳定可靠,安全合规的短信触达服务。用户可快速接入,调用 API / SDK 或者通过控制台即可发送,支持发送验证码、通知类短信和营销短信。国内验证短信秒级触达,99%到达率;国际/港澳台短信覆盖全球200+国家/地区,全球多服务站点,稳定可靠。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档