专栏首页京程一灯JavaScript 有个 Unicode 的天坑

JavaScript 有个 Unicode 的天坑

最近笔者在项目中遇到了emoji表情的处理,期间发现js处理多字节字符时会有较多坑,记录一下与各位分享。

本文涉及知识点:

Unicode (BMP/SP) UTF-8 UTF-16 UTF-32 UCS-2 javascript字符处理

Unicode

Unicode是目前绝大多数程序使用的字符编码,定义也很简单,用一个码点(code point)映射一个字符。码点值的范围是从U+0000到U+10FFFF,可以表示超过110万个符号。下面是一些符号与它们的码点

  • A的码点 U+0041
  • a的码点 U+0061
  • ©的码点 U+00A9
  • ☃的码点 U+2603
  • ?的码点 U+1F4A9

对于每个码点,Unicode还会配上一小段文字说明,可以在codepoints.net查到,比如 ?的码点说明

Unicode最前面的65536个字符位,称为基本平面(BMP-—Basic Multilingual Plane),它的码点范围是从U+0000到U+FFFF。最常见的字符都放在这个平面,这是Unicode最先定义和公布的一个平面。

剩下的字符都放在补充平面(Supplementary Plane),码点范围从U+010000一直到U+10FFFF,共16个。

UTF与UCS

UTF(Unicode transformation format)Unicode转换格式,是服务于Unicode的,用于将一个Unicode码点转换为特定的字节序列。常见的UTF有

UTF-8 可变字节序列,用1到4个字节表示一个码点 UTF-16 可变字节序列,用2或4个字节表示一个码点 UTF-32 固定字节序列,用4个字节表示一个码点

UTF-8对ASCⅡ编码是兼容的,都是一个字节,超过U+07FF的部分则用了复杂的转换方式来映射Unicode,具体不再详述。

UTF-16对于BMP的码点,采用2个字节进行编码,而BMP之外的码点,用4个字节组成代理对(surrogate pair)来表示。其中前两个字节范围是U+D800到U+DBFF,后两个字节范围是U+DC00到U+DFFF,通过以下公式完成映射(H:高字节 L:低字节 c:码点)

H = Math.floor((c-0x10000) / 0x400)+0xD800

L = (c – 0x10000) % 0x400 + 0xDC00

比如 ? 用UTF-16表示就是”\uD83D\uDCA9″

UCS(Universal Character Set)通用字符集,是一个ISO标准,目前与Unicode可以说是等价的。

相对于UTF,UCS也有自己的转换方法(编码)。如

UCS-2 用2个字节表示BMP的码点 UCS-4 用4个字节表示码点

UCS-2是一个过时的编码方式,因为它只能编码基本平面(BMP)的码点,在BMP的编码上,与UTF-16是一致的,所以可以认为是UTF-16的一个子集。

UCS-4则与UTF-32等价,都是用4个字节来编码Unicode。

javascript字符处理

辣莫,js到底是用的啥编码呢?答案是UCS-2。咦,刚刚不是说UCS-2过时了吗?首先看下年表

1990 UCS-2 诞生 1995.5 JavaScript 诞生 1996.7 UTF-16 诞生

也就是说,Brendan Eich在写JS的时候,UTF-16还没问世,所以只能用UCS-2的方式来处理字符,也因此留下了隐患。

坑1——length属性

先看一个简单的例子:

>”\uD83D\uDCA9″ === “?” >true >”?”.length >2

因为”?”在JS的编码是”\uD83D\uDCA9″,而JS认为每16位(2字节)即表示一个字符,所以一坨大便是占2个字符的。我们经常用length来判断字符串长度,那产品不干了呀,说好可以输入10个字,为毛输了5个emoji就不给输入了?

怎么破?可以用万能的正则匹配

var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; // 匹配UTF-16的代理对 function countSymbols(string) { return string // 把代理对改为一个BMP的字符. .replace(regexAstralSymbols, '_') // …这时候取长度就妥妥的啦. .length; } countSymbols('?'); // 1

坑2——反转字符串

js里怎么反转(reverse)字符串?相信有些同学已经想到了一个极简的方案

function reverse(str) { return str.split('').reverse().join(''); }

js虽没有直接的反转字符串的API,但是数组有啊,转数组反转之后再转回字符串,嘿嘿嘿,是不是很机智?这时候Unicode大爷又出来打脸了:你们呐,sometimes naive!

拿刚才的函数反转带有?的字符串试试

reverse('这是一坨?') "��坨一是这"

�的Unicode码点是+UFFFD,通常用来表示Unicode转换时无法识别的字符(也就是乱码)

当?(\uD83D\uDCA9)通过上述方法反转时,变成\uDCA9\uD83D,不是一个合法的代理对(高低字节范围不同),同时,Unicode规定代理对范围内的码点不能单独出现,所以js只能用�表示了。

怎么破?

ES6的Array.from支持代理对的解析

function reverse(string) { return Array.from(string).reverse().join(''); }

使用 Esrever (reverse反转之后就是esrever…)

坑3——码点与字符互转

String.fromCharCode可以将一个码点转换为字符,比如

String.fromCharCode(0x0041) 'A'

但超过BMP平面的就跪了。

>> String.fromCharCode(0x1F4A9) // U+1F4A9 '' // U+F4A9, not U+1F4A9

事实上这个API是支持俩参数的,分别是代理对的高低字节。所以需要通过公式计算出对应的高低字节

>> String.fromCharCode(0xD83D, 0xDCA9) '?' // U+1F4A9 >> '?'.charCodeAt(0) 0xD83D

一个字,蛋疼!

怎么破? ES6大法好。

>> String.fromCodePoint(0x1F4A9) '?' // U+1F4A9 >> '?'.codePointAt(0) 0x1F4A9

坑4——正则匹配

正则匹配符.只能匹配单个“字符”,但js将代理对当成两个单独的“字符”处理,所以匹配不到任何辅助平面字符。

>> /foo.bar/.test('foo?bar') false

思考一下,什么正则表达式可以表示任何Unicode字符? 显然.是不够的,因为它不能匹配辅助平面字符或者换行符。那么用\s\S呢?

>> /^[\s\S]$/.test('?') false

怀疑人生了~~正确的匹配任意Unicode字符的正则如下:

>> /[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/.test('?') // wtf true

怎么破? ES6给出一个简单的方法——增加一个u标志

>> /foo.bar/u.test('foo?bar') true

注意:这里的.还是不能匹配换行符。

ES6的Unicode支持

从上面的例子中可以看出,ES6已经在很努力地填坑了。对于Unicode字符,ES6支持新的表示方法 \u{1F4A9} 加上花括号后,可以把码点直接填进去来表示,而不用去计算代理对。再补充2点:

1. 为了向后兼容,字符串的length属性还是用双字节判断的,所以要用Array.from(str).length。

2. 遍历字符串的时候,可以用for(let s of str) {}


往期精选文章

ES6中一些超级好用的内置方法

浅谈web自适应

使用Three.js制作酷炫无比的无穷隧道特效

一个治愈JavaScript疲劳的学习计划

全栈工程师技能大全

WEB前端性能优化常见方法

一小时内搭建一个全栈Web应用框架

干货:CSS 专业技巧

四步实现React页面过渡动画效果

让你分分钟理解 JavaScript 闭包



小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。

本文分享自微信公众号 - 京程一灯(jingchengyideng)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-09-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • GitHub 12个实用技巧

    在GitHub上打开一个文件(任何仓库的任何文件),在页面的右上角有一个像小铅笔的按钮。点击它,你就可以编辑文件了。当你编辑完成,点击Propose file ...

    疯狂的技术宅
  • CSS 框架 Bulma 教程

    Bootstrap 是最著名的 CSS 框架,但是今天我想推荐另一个更轻量化、更易用的框架----Bulma。有了它,即使完全不懂 CSS,也可以轻而易举做出美...

    疯狂的技术宅
  • 一道 React 面试题:在浏览器、组件和元素中都渲染了些什么?

    从技术上来说,ReactDOM 不会在 DOM 中渲染 React 组件或 React 元素。它渲染由其组件实例支持的 DOM 元素。对于类组件来说这是正确的。...

    疯狂的技术宅
  • Unicode与JavaScript详解

    上个月,我做了一次分享,详细介绍了Unicode字符集,以及JavaScript语言对它的支持。下面就是这次分享的讲稿。 ? 一、Unicode是什么? Uni...

    ruanyf
  • Unicode与JavaScript详解

    上个月,我做了一次分享,详细介绍了Unicode字符集,以及JavaScript语言对它的支持。下面就是这次分享的讲稿。 ![](/blogimg/asset/...

    ruanyf
  • 今日推荐fucking-algorithm

    最近应该也是跳槽季,很多公司都大换血了,也有很多同行开始展望新的机会,在这个情况下,当然少不了刷题临时抱佛脚了。

    仇诺伊
  • 清官谈mysql中utf8和utf8mb4区别,请使用utf8mb4

    MySQL在5.5.3之后增加了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。好在utf8mb4...

    用户7657330
  • 这难道是UTF-8字符编码的设计缺陷?

    UTF-8都知道是啥,所有人都在用它来存储和传输文本。鄙人闲来无事研究了一下utf8的规格,发现它并没有想象中的那样完美。

    Jean
  • Android Studio 学习笔记

    最近从Eclipse转到Android Studio IDE,很多东西需要学习,本文是个记录。

    zhangyunfeiVir
  • 机器学习性能评价指标汇总

    AUC 是 ROC (Receiver Operating Characteristic) 曲线以下的面积, 介于0.1和1之间。Auc作为数值可以直观的评价分...

    莫斯

扫码关注云+社区

领取腾讯云代金券