前言

这个问题在最近经常被刷到,虽然我知道怎么解决这个问题,但是为什么会这样,其实并不太明白,所以这次特此开一篇文章讲讲为什么length获取字符位数会不准的问题,以及我们的解决办法。

"𠮷".length    //2

"💩".length   //2

𠮷与吉利的吉是一个意思,𠮷是异体写法,古文中会出现。

按照我们的认知,上述的length输出应该是1才对,但事实上却是2。

计算机是如何存储字符的

早期的ASCII字符集

在计算机中,所有的数据存储最终都是二进制,如何正确的存储及其识别字符,做法就是定制一个编码规则,让每个字符都由一个个数字来表示,这些数字被称为码点,然后计算机将码点转为二进制存储起来,实现了字符的识别与存储。

我们最熟知的应该就是 ASCII字符集,所谓的字符集就是一套字符与码点的规则,ASCII字符集定义了128个字符,码点是0-127,包含了英文字母大小写,数字0-9,以及一些特殊符号。

Unicode字符集

随着计算机的发展,各个国家都开始使用,但是每个国家都有自己的语言文字,显然ASCII字符集已经无法满足人们的需要了,于是各个国家都开始定制自己的字符集,比如中国的GB2312、GBK,虽然这样确实解决了各自的需要,但是带来了一个头疼的问题。

就是每个国家的字符集都不一样 ,如果使用不同的字符集软件打开,文本就会发生乱码,因为码点与字符根本不是同一个内容。

早期常见的乱码:锟斤拷

编码:将码点与字符进行相互转换的方式被称为编码。

为了解决不同语言环境下的字符转换问题,1990年开始研发,1994年正式公布的Unicode字符集,至今还在维护更新,被称为万国码,它支持不同国家的语言文字,且是唯一码点,这就可以在不同的语言环境下,使用同一种字符集,展示相同的结果,是最为广泛使用的字符集。

UTF 编码规则

Unicode字符集解决了字符转换标准的问题,但是并没有规定如何存储与转换,这个规则则是由UTF标准来实现。

UTF 是 Unicode Transformation Format 的缩写,意思是“Unicode转换格式”,后面的数字表明至少使用多少个比特位(Bit)来存储字符。

简单来讲, UTF 标准规定了在存储和传输过程中,需要将一个字节、两个字节还是四个字节解析为一个 Unicode 码点。

常见的几种编码格式:

  1. utf-8
  2. utf-16
  3. utf-32

utf-32

我们需要知道,计算机中最小的存储单位是8bit,也就是一个字节,utf-32使用32个bit来表示一个字符,当我们选择了utf-32时,计算机就会在读取二进制时,会每32个bit做一次截断,从而正确的分割每个字符。

32位的编码方式虽然有非常大的空间来表示众多字符,甚至未来的变化,但是它有一个很明显的缺点,如果我是一个码点为1的字符,1在二进制中是不需要换算的,那么他最终会存储为:

00000000000000000000000000000001

很明显会占据非常多的存储空间,这对于使用英语的人而言就不太好了,他需要比ASCII字符还要多出3倍的空间来存储,因为ASCII码中1个字符只需要1个字节的存储空间就足够了。

utf-8

为了节省字符存储的空间,出现了utf-8,他是一种可变长度的编码规则,如果码点足够小,就可以使用1字节来表示,如果很大,可以往上扩大,utf-8区间在1-4个字节。

由于可以1字节存储,所以utf-8是兼容ASCII字符集的,要问为什么?那你就要想一下为啥计算机核心开发语言现在都还是英语,人家就是有这个优势,优先权。

具体的编码细节就不多说了,以后可以学习,现在用不到。

utf-16

16是介于8-32之间的一种编码规则,它使用2或者4个字节来存储字符,这也是我们JavaScript中String类型采用的规则,为什么js使用该规则,我们通过历史来了解一下原因。

JavaScript诞生于1995年5月,在这个时间段除了Unicode字符集还有另一支团队也想统一字符集,他们就是1989年成立的UCS团队,他们于1990年发布出UCS-2标准,于是我们的祖师爷选择了UCS-2。(注意那会都还没有互联网哦)

但是Unicode团队与UCS团队很快就发现了彼此,于是他们达成一致世界上不需要两套统一字符集;1991年10月,两个团队决定合并字符集,从今以后只有一套字符集,那就是Unicode,并且修订此前发布的字符集,UCS的码点将与Unicode完全一致。

但是UCS的开发是进度快于Unicode,早先就发布了UCS-2标准,合并后使得UTF-16延迟到1996年7月才公布,明确宣布是UCS-2的超集,即基本平面字符沿用UCS-2编码,辅助平面字符定义了4个字节的表示方法。

于是我们的JavaScript不得不切换到UTF-16了,这也是为什么我们的js使用该标准的原因。

什么是平面?
我们知道 Unicode 是一本很厚的字典,其中定义了世界上所有的字符,为了给这些不同的字符进行分类。Unicode 使用了分区定义的方式,每个区可以存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有 17 个(2^5)平面。划分如下:
0x0000~0xFFFF:第0平面,基本多文种平面(Basic Multilingual Plane, BMP)
0x10000~0x1FFFF:第1辅助平面,多文种补充平面(Supplementary Multilingual Plane, SMP)
0x20000~0x2FFFF:第2辅助平面,表意文字补充平面(Supplementary Ideographic Plane, SIP)
0x30000~0x3FFFF:第3辅助平面,表意文字第三平面(Tertiary Ideographic Plane, TIP)
0x40000~0xDFFFF:第4-13辅助平面,尚未使用
0xE0000~0xEFFFF:第14辅助平面,特别用途补充平面(Supplementary Special-purpose Plane, SSP)
0xF0000~0xFFFFF:第15辅助平面,保留作为私人使用区(Private Use Area, PUA)
0x100000~0x10FFFF:第16辅助平面,保留作为私人使用区(Private Use Area, PUA)

utf-16的编码规则:

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

简单点来说,如果这个字符超出了基本平面,会使用2个码元来表示。

什么是码元?

在计算机存储和网络传输中,码点值会被映射到一个或者多个码元,码元可以理解为字符编码时的最小基本单元(单位),

为什么length会不对?

从utf-16中我们可以得知,超出基本平面的字符会使用2个码元来表示,最开头的𠮷💩就是超出了基本平面。

已知:1个码元 = 16bit

在ECMAScript规范中,String类型是由0个或者多个16位无符号整数值(元素)组成的所有有序序列的集合。String中每个元素都被视为UTF-16代码单元值。每个元素都被视为在序列中占据一个位置。

The String type is the set of all ordered sequences of zero or more 16-bit unsigned integer values (“elements”) up to a maximum length of 253-1 elements. The String type is generally used to represent textual data in a running ECMAScript program, in which case each element in the String is treated as a UTF-16 code unit value. Each element is regarded as occupying a position within the sequence. These positions are indexed with nonnegative integers. The first element (if any) is at index 0, the next element (if any) at index 1, and so on. The length of a String is the number of elements (i.e., 16-bit values) within it. The empty String has length zero and therefore contains no elements.

翻一下人话就是:String类型中每个UTF-16的码元被视为1个字符。一个码元被视为占据一个位置。

𠮷💩因为超出基本平面,那么就会转换为2个码元,然后string类型一个码元占据一个位置,这就导致了我们的length得到的是2。

如何避免length不准的问题

早期的js并没有考虑到会有这么多字符,这就导致很多方法使用2字节也就是1个码元的方式来进行操作。

for循环

var str = '👻yo𠮷'
for (var i = 0; i < str.length; i ++) {
  console.log(str[i])
}

// -> �
// -> �
// -> y
// -> o
// -> �
// -> �

如果我们需要遍历字符串,可以使用es6的for-of

var str = '👻yo𠮷'
for (const char of str) {
  console.log(char)
}

// -> 👻
// -> y
// -> o
// -> 𠮷

length

"𠮷".length    //2

我们可以通过es6扩展运算符的方式拿到正确的length

[..."𠮷"].length   //1

正则表达式 U

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

/^.$/.test('👻')
// -> false

/^.$/u.test('👻')
// -> true

charCodeAt

charCodeAt用于获取给定索引处的码点值(默认索引为0),返回065535 之间的整数,也就是说他的正确返回值只能是基本平面的字符,如果超出了,得到的值就会超出预期。

"啊".charCodeAt();  //21834

"💩".charCodeAt();  //55357

我们再通过fromCharCode将码点编码为字符串:

String.fromCharCode(21834);   //啊

String.fromCharCode(55357);   //\uD83D

可以看到第二个就无法被成功转换,它得到只是第一个码元数字。

我们可以通过es6新增的codePointAt方法来获取正确的码点值

"啊".codePointAt();  //21834

"💩".codePointAt();  //128169
String.fromCharCode(21834);   //啊

String.fromCharCode(128169);   //💩

字符相等判断

许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法:

一种是直接生成:\u01D1

"\u01D1";   //'Ǒ'

另一种是合成的方式:

"\u004F";  //'O'
"\u030C"; //'ˇ'

"\u004F\u030C";  //'Ǒ'

虽然两个字符从字面意思上一样的,但是js判断全等得到是false。

'Ǒ' === 'Ǒ'    //false

为了解决这个问题,es6提供了normalize方法,用于将字符的不同的表示方式统一为同样的形式,这称之为Unicode 正规化。

'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true

'Ǒ'.normalize() === 'Ǒ'.normalize()    
//true

localeCompare

在js中我们可以通过localeCompare来对字符串进行排序判断,通常这个方法会配合sort来进行使用,localeCompare会返回一个数字来指示一个参考字符串是否在排序顺序前面或之后或与给定字符串相同。

返回值: 1、0、-1

在了解了上面码点之后,我认为localeCompare也是通过码点来实现的判断,但是结果并不是。

unicode是用于文字的编码,他是无法区分国家的,而localeCompare是一种基于国际化字体的地区字符比较,例如中国用中文,美国用英文,法国用法文,德国用德文。。将这些国家的文字按照国家/地区等进行编号,然后每个编号都对应了该国地区的文字。

他和unicode完全是两个东西,我们来看一段代码就可以很直观的明白:

'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').sort((a,b)=>a.localeCompare(b)).join('');
//结果:0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

而我们数组的sort方法在不传入判断函数时,其实默认就是使用的unicode码点进行判断:

'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').sort().join('');
//'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'

得到的结果是完全不一样的。

由于localeCompare是根据国家来进行区分判断的,这就导致可能在一些不同语言的情况下,排序的结果会发生变化,好在localeCompare本身是支持传入国家参数的,如果可以,我们可以统一一个国家,以免后续排序出现错误。

具体国家参数参考文档: localeCompare

结语

至此基本就是我总结的一些关于length属性的相关内容,看完应该会有一个比较全局的认知,在一些代码编写上,可以更加完善,比如input字符数的统计之类的。

分类: 前端功能 标签: lengthUnicodeASCII字符集UTFutf-8utf-16utf-32码元码点平面

评论

暂无评论数据

暂无评论数据

目录