Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >为什么 "𠮷𠮷𠮷".length !== 3 ?

为什么 "𠮷𠮷𠮷".length !== 3 ?

作者头像
良月柒
发布于 2024-06-21 08:50:18
发布于 2024-06-21 08:50:18
12400
代码可运行
举报
运行总次数:0
代码可运行
程序员的成长之路

互联网/程序员/技术/资料共享

关注

阅读本文大概需要 5 分钟。

来自:https://juejin.cn/post/7025400771982131236

推荐一个我自己写的Chrome插件:

https://briefurl.top/csdn

免登录一键复制代码,支持选中代码,自动展开 CSDN 全文,去除烦人的登录弹窗。

这是转换的短链,会转到google插件商店去,访问需要梯子哈。

以下是正文。


在开发过程中偶尔会遇到关于编码、Unicode,Emoji 的问题,发现自己对这方面的基础知识并没有充分掌握。所以在经过一番查找学习之后,整理几篇通俗易懂的文章分享出来。

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。j

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'吉'.length
// 1

'𠮷'.length
// 2

'❤'.length
// 1

'💩'.length
// 2

要解释这个问题要从 UTF-16 编码说起。

UTF-16

从 ECMAScript® 2015 规范中可以看到,ECMAScript 字符串使用的是 UTF-16 编码。

定与不定: UTF-16 最小的码元是两个字节,即使第一个字节可能都是 0 也要占位,这是固定的。不定是对于基本平面(BMP)的字符只需要两个字节,表示范围 U+0000 ~ U+FFFF,而对于补充平面则需要占用四个字节 U+010000~U+10FFFF。

之前有介绍过 utf-8 的编码细节,了解到 utf-8 编码需要占用 1~4 个字节不等,而使用 utf-16 则需要占用 2 或 4 个字节。来看看 utf-16 是怎么编码的。

UTF-16 的编码逻辑

UTF-16 编码很简单,对于给定一个 Unicode 码点 cp(CodePoint 也就是这个字符在 Unicode 中的唯一编号):

  1. 如果码点小于等于 U+FFFF(也就是基本平面的所有字符),不需要处理,直接使用。
  2. 否则,将拆分为两个部分 ((cp – 65536) / 1024) + 0xD800,((cp – 65536) % 1024) + 0xDC00 来存储。

Unicode 标准规定 U+D800...U+DFFF 的值不对应于任何字符,所以可以用来做标记。

举个具体的例子:字符 A 的码点是 U+0041,可以直接用一个码元表示。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'\u0041'
// -> A

A === '\u0041'
// -> true
代码语言:javascript
代码运行次数:0
运行
复制

Javascript 中 \u 表示 Unicode 的转义字符,后面跟着一个十六进制数。

而字符 💩 的码点是 U+1f4a9,处于补充平面的字符,经过 👆 公式计算得到两个码元 55357, 56489 这两个数字用十六进制表示为 d83d, dca9,将这两个编码结果组合成代理对。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'\ud83d\udca9'
// -> '💩'

'💩' === '\ud83d\udca9'
// -> true
代码语言:javascript
代码运行次数:0
运行
复制

由于 Javascript 字符串使用 utf-16 编码,所以可以正确将代理对 \ud83d\udca9 解码得到码点 U+1f4a9。

还可以使用 \u + {},大括号中直接跟码点来表示字符。看起来长得不一样,但他们表示的结果是一样的。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'\u0041' === '\u{41}'
// -> true

'\ud83d\udca9' === '\u{1f4a9}'
// -> true

可以打开 Dev Tool 的 console 面板,运行代码验证结果。

所以为什么 length 判断会有问题?

要解答这个问题,可以继续查看 规范,里面提到:在 ECMAScript 操作解释字符串值的地方,每个元素都被解释为单个 UTF-16 代码单元。

Where ECMAScript operations interpret String values, each element is interpreted as a single UTF-16 code unit.

所以像💩 字符实际上占用了两个 UTF-16 的码元,也就是两个元素,所以它的 length 属性就是 2。(这跟一开始 JS 使用 USC-2 编码有关,当初以为 65536 个字符就可以满足所有需求了)

但对于普通用户而言,这就完全没办法理解了,为什么明明只填了一个 '𠮷',程序上却提示占用了两个字符长度,要怎样才能正确识别出 Unicode 字符长度呢?

我在 Antd Form 表单使用的 async-validator 包中可以看到下面这段代码

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const spRegexp = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;

if (str) {
  val = value.replace(spRegexp, '_').length;
}
代码语言:javascript
代码运行次数:0
运行
复制

当需要进行字符串长度的判断时,会将码点范围在补充平面的字符全部替换为下划线,这样长度判断就和实际显示的一致了!!!

ES6 对 Unicode 的支持

length 属性的问题,主要还是最初设计 JS 这门语言的时候,没有考虑到会有这么多字符,认为两个字节就完全可以满足。所以不止是 length,字符串常见的一些操作在 Unicode 支持上也会表现异常。

下面的内容将介绍部分存在异常的 API 以及在 ES6 中如何正确处理这些问题。

for vs for of

例如使用 for 循环打印字符串,字符串会按照 JS 理解的每个“元素”遍历,辅助平面的字符将会被识别成两个“元素”,于是出现“乱码”。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var str = '👻yo𠮷'
for (var i = 0; i < str.length; i ++) {
  console.log(str[i])
}

// -> �
// -> �
// -> y
// -> o
// -> �
// -> �
代码语言:javascript
代码运行次数:0
运行
复制

而使用 ES6 的 for of 语法就不会。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var str = '👻yo𠮷'
for (const char of str) {
  console.log(char)
}

// -> 👻
// -> y
// -> o
// -> 𠮷
代码语言:javascript
代码运行次数:0
运行
复制

展开语法(Spread syntax)

前面提到了使用正则表达式,将辅助平面的字符替换的方式来统计字符长度。使用展开语法也可以得到同样的效果。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[...'💩'].length
// -> 1
代码语言:javascript
代码运行次数:0
运行
复制

slice, split, substr 等等方法也存在同样的问题。

正则表达式 u

ES6 中还针对 Unicode 字符增加了 u 描述符。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/^.$/.test('👻')
// -> false

/^.$/u.test('👻')
// -> true
代码语言:javascript
代码运行次数:0
运行
复制

charCodeAt/codePointAt

对于字符串,我们还常用 charCodeAt 来获取 Code Point,对于 BMP 平面的字符是可以适用的,但是如果字符是辅助平面字符 charCodeAt 返回结果就只会是编码后第一个码元对于的数字。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'羽'.charCodeAt(0)
// -> 32701
'羽'.codePointAt(0)
// -> 32701

'😸'.charCodeAt(0)
// -> 55357
'😸'.codePointAt(0)
// -> 128568
代码语言:javascript
代码运行次数:0
运行
复制

而使用 codePointAt 则可以将字符正确识别,并返回正确的码点。

String.prototype.normalize()

由于 JS 中将字符串理解成一串两个字节的码元序列,判断是否相等是根据序列的值来判断的。所以可能存在一些字符串看起来长得一模一样,但是字符串相等判断结果确是 false。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'café' === 'café'
// -> false
代码语言:javascript
代码运行次数:0
运行
复制

上面代码中第一个 café 是有 cafe 加上一个缩进的音标字符\u0301组成的,而第二个 café 则是由一个 caf + é 字符组成的。所以两者虽然看上去一样,但码点不一样,所以 JS 相等判断结果为 false。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'cafe\u0301'
// -> 'café'

'cafe\u0301'.length
// -> 5

'café'.length
// -> 4

为了能正确识别这种码点不一样,但是语意一样的字符串判断,ES6 增加了 String.prototype.normalize 方法。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'cafe\u0301'.normalize() === 'café'.normalize()
// -> true

'cafe\u0301'.normalize().length
// -> 4
代码语言:javascript
代码运行次数:0
运行
复制
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的成长之路 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JavaScript 有个 Unicode 的天坑
最近笔者在项目中遇到了emoji表情的处理,期间发现js处理多字节字符时会有较多坑,记录一下与各位分享。
疯狂的技术宅
2019/03/28
1.1K0
字符编码技术专题(五):前端必读的计算机字符编码知识入门
以上这些问题都涉及到计算机中*础的知识点——字符集及字符编码的概念,本篇将从前端开发人员的视解,让你彻底搞清并理解这些问题的本质。
JackJiang
2023/09/28
4040
字符编码技术专题(五):前端必读的计算机字符编码知识入门
2020-2-22-Unicode代理对(utf-16)
为什么说是大部分呢?因为还有一个神奇的区域,叫做Unicode代理对。它们需要使用4个字节来表示一个字符。
黄腾霄
2020/06/10
1.6K0
JavaScript emoji utils
也就是说,Unicode支持的编码范围是U+0000到U+10FFFF,能对应100多万个符号(0x10FFFF === 1114111)。这些符号被分组归入16个平面(panel),所以每个平面放65536(16^4 === 65536)个
ayqy贾杰
2019/06/12
2.1K0
JavaScript emoji utils
字符编码的那些事
之前看到ES6中对String扩展了不少新特性,字符串操作更加友好,比如"\u{1f914}",codePointAt(),String.fromCodePoint()。其中涉及到不少字符编码的知识,为了更好理解这些新特性,本文对字符编码相关知识做一个较全面的梳理和总结。
elson
2018/06/17
1.9K0
【原创】经验分享:一个小小emoji尽然牵扯出来这么多东西?
商品评价列表页,显示每条用户的评价详情,为了保护用户隐私,要求显示用户昵称时只能显示第一位和最后一位,其他的用※代替。
一枝花算不算浪漫
2020/10/09
8720
【原创】经验分享:一个小小emoji尽然牵扯出来这么多东西?
聊聊Java中codepoint和UTF-16相关的一些事
Unicode和UTF-8/UTF-16/UTF-32之间就是字符集和编码的关系。字符集的概念实际上包含两个方面,一个是字符的集合,一个是编码方案。字符集定义了它所包含的所有符号,狭义上的字符集并不包含编码方案,它仅仅是定义了属于这个字符集的所有符号。但通常来说,一个字符集并不仅仅定义字符集合,它还为每个符号定义一个二进制编码。当我们提到GB2312或者ASCII的时候,它隐式地指明了编码方案是GB2312或者ASCII,在这些情况下可以认为字符集与编码方案互等。
哲洛不闹
2018/09/14
1.2K0
聊聊Java中codepoint和UTF-16相关的一些事
从 JS 里的 MD5 转换踩坑开始说起
写 JS 代码的同学们不知道有没有注意过,后台接口通过 JSON 处理汉字字符、emoji 时,返回的是像 \u00ff 这样转义处理的字符,而不是它们的明文原文。这是为什么呢?
贤羽
2022/06/09
2.7K2
字符串的新增方法
ES5 提供String.fromCharCode()方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF的字符。
小小杰啊
2022/12/21
5700
从JavaScript看字符编码的前世今生!
导语 | 每个程序员都应该了解一下字符编码,有了基础概念之后我们对编程语言、字符处理能有更深入的理解。本文我花了大量时间进行资料查阅和考证,希望能够给大家带来一些帮助,多多交流! 一、起因 最近在研究Babel的源码,在看到Acorn词法解析源码中有这样一段逻辑: pp.fullCharCodeAtPos = function() { let code = this.input.charCodeAt(this.pos) if (code <= 0xd7ff || code >= 0xdc00
腾讯云开发者
2022/05/18
8400
从JavaScript看字符编码的前世今生!
Unicode与JavaScript详解
上个月,我做了一次分享,详细介绍了Unicode字符集,以及JavaScript语言对它的支持。下面就是这次分享的讲稿。 一、Unicode是什么? Unicode源于一个很简单的想法:将全世界所有的
ruanyf
2018/04/12
7650
Unicode与JavaScript详解
ES6 字符串新增方法
「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
用户4793865
2023/01/12
4470
Unicode 与 utf8 utf16 utf32的关系
Unicode是计算机领域的一项行业标准,它对世界上绝大部分的文字的进行整理和统一编码,Unicode的编码空间可以划分为17个平面(plane),每个平面包含2的16次方(65536)个码位。17个平面的码位可表示为从U+0000到U+10FFFF,共计1114112个码位,第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800到U+DFF
2018/05/28
2.2K0
Java的String类中提到的代码点,代码单元到底是什么?
unicode是计算机科学领域里的一项业界标准,包括字符集、编码方案等。计算机采用八比特一个字节,一个字节最大整数是255,还要表示中文一个字也是不够的,至少需要两个字节,为了统一所有的文字编码,unicode为每种语言中的每个字符设定了统一并且唯一的二进制编码,通常用两个字节表示一个字符,所以unicode每个平面可以组合出65535种不同的字符,一共17个平面。
JavaEdge
2020/05/26
5270
JavaScript 如何实现在搜索中忽略变音符号
变音符号是指添加在字母上面的符号,以更改字母的发音或者以区分拼写相似词语。例如汉语拼音字母ü上面的两个小点,或á、à字母上面的标调符。
沈唁
2020/12/25
9010
UTF8编码的原理及白名单过滤utf8mb4(Caused by: java.sql.BatchUpdateException: Incorrect string value)
网上提供了大部分的解决方法是修改数据库配置,但是数据库如果使用连接池,无法保证其他连接时不指定utf-8,所以避免不了其他连接污染连接池。这里给出另一种解决方法,过滤掉特殊字符。
mingjie
2022/05/12
1.2K0
《流畅的Python》第四章学习笔记
在Windows上使用open打开utf-8编码的txt文件时开头会有一个多余的字符\ufeff,它叫BOM,是用来声明编码等信息的,但python会把它当作文本解析。
zx钟
2020/12/11
5980
《流畅的Python》第四章学习笔记
【译】《Understanding ECMAScript6》- 第一章-基础知识(一)
目录: 更好的Unicode编码支持 codePointAt()函数 String.fromCodePoint() 用转义序列对Non-BMP字符编码 normalize()函数 正则表达式的u标志 Unicode标识符 更多字符串相关改动 includes(),startsWith(),endsWith() repeat() 更多的正则表达式改动 正则表达式的y标志 克隆正则表达式 flags属性 Object.is() ES6在ES5的基础上做了大量的改动,有一些较大的改动涉及到新的数据类型和语法
寒月十八
2018/01/30
1.2K0
小览 ES6-ES2019 中正则表达式的新发展
在此前的 《JS正则表达式--从入门到精分》 一文中,曾经较完整的介绍过 Javascript 中正则表达式的用法。而从 ES6(ES2015) 开始,借助 Babel 等标志性的工具,JS 的发展似乎也不想重蹈 Flash 时代的无所作为,走上了每年一个小版本的快车道;在此过程中,正则表达式也陆续演化出一些新的特性。
江米小枣
2020/06/15
6760
今天一次把 Unicode 和 UTF-8 说清楚
在日常开发过程中,Unicode & UTF-8 并不是很受关注的知识,但在阅读源码或文章时,出现频率很高。如果你没有理解清楚 Unicode、UTF-8、UTF-16 和 UTF-32 之前的关系,会带来阅读障碍。在这篇文章里,我将带你理解 Unicode 字符集的原理,希望能帮上忙。
用户9995743
2022/09/26
1.2K0
今天一次把 Unicode 和 UTF-8 说清楚
相关推荐
JavaScript 有个 Unicode 的天坑
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验