JavaScript 转义和编码

4,171 阅读16分钟

JavaScript中转义字符的使用

当使用特殊字符(如单引号、双引号、撇号和&符号)时,将使用转义字符(反斜杠)。在字符前放置反斜杠,使其显示。 说起转义字符,大家最先想到的肯定是使用反斜杠,这也是我们最常见的,很多编程语言都支持。 除了反斜杠以外,在前端开发中,还有其他几种转义字符,也是较常见的, 转义字符从字面上讲,就是能够转变字符原本的意义,得到新的字符。常用在特殊字符的显示以及特定的编码环境中。

字符串中的转义

使用反斜杠来表示转义字符时,主要是在字符串中使用。这里就需要了解字符集和编码等知识,具体可见 前端开发中需要搞懂的字符编码。 字符集就是字符的集合,最常见的 :

  • ASCII字符集 :ASCII的任一个字符都可以被转义,使用的就是反斜杠加上数字编码,特殊的也能使用反斜杠加上字母。
  • Unicode字符集等:Unicode,也能进行转义,使用则是反斜杠加上码点。由于Unicode包含了ASCII的所有字符,且编码一致,所以都可算编码转义,而现在前端编程所涉及到的已经都是Unicode字符。

一般,反斜杠(\)在字符串中有特殊的含义,用来和后面的字符一起组合表示一些特殊字符,所以又被称为转义符。 反斜杠后面可以跟着的字符大致有以下几种:

  • 字母
  • 三位八进制
  • x 加上 两位十六进制
  • Unicode码点

字母

转义字符中最基础的就是:使用反斜杠\加上字母,表示那些无法输入表示特殊含义的字符,常见的有以下几种:

  • \b 后退键
  • \f 换页符
  • \n 换行符
  • \r 回车键
  • \t 制表符
  • \v 垂直制表符

以前在字符串拼接时,就经常使用\n、\t:

console.log("一缕清风\nKen")

/*  
一缕清风
Ken
*/

需要注意的是,这些字母是特殊的可应用于转义的字母。 如果是非特殊字母,加上反斜杠,则会忽略反斜杠,很多字符也是同样忽略反斜杠:

console.log('\a');  // a
console.log('\"');  // "
console.log('\?');  // ?

console.log('\\a');  // \a
console.log('\\"');  // \"
console.log('\\?');  // \?

八进制数字

反斜杠后面跟3位八进制数,就代表一个转义字符:

console.log('\106');         // F
console.log('\165');         // u
console.log('\251');         // ©

取值范围:000-377,总共也是有256种字符,其中就包含了所有的ASCII码。 八进制和十六进制能转义的字符是一样的,进行进制转换即可。搞懂JavaScript中的进制与进制转换 在JS中,用这两种方式的转义字符是相等的:

console.log('\xA9' === '\251') // true
console.log('\200' === '\x80') // true

十六进制数字

十六进制更常用,它的转义规则:\x<hex>,\x后跟上2位十六进制数。 因为只有两位,范围是:0x00-0xFF,所以这种方式也只能输出265种字符,其中:

  • 0x00-0x7F 和ASCII码一致
  • 0x80-0x9F 表示控制字符
  • 0xA0-0xFF 表示文字字符
// 0x00-0x7F 和ASCII码一致
// 0x80-0x9F 表示控制字符
// 0xA0-0xFF 表示文字字符
console.log('\x00') 
console.log('\x7F') 
console.log('\x80') 
console.log('\x9F') 
console.log('\xA0') 
console.log('\xFF') 

console.log('\x67')    // g
console.log('\x75');   // u
console.log('\xA9');   // ©

Unicode码点

提到Unicode,首先需明确的一点,JS中的字符串是基于 Unicode 的 UTF-16 编码方式。 Unicode字符规定了码点和字符平面。 码点使用从 U+0000 到 U+10FFFF 的方位来表示所有的字符。 如果直接使用码点来转义所有的Unicode字符,则使用规则:\u{<hex>},\u后跟上1-6位的十六进制:

console.log('\u{A9}')     // ©
console.log('\u{597d}')   // 好
console.log('\u{1f604}')  // 😄

字符平面又分为基本平面和非基本平面。 其中对于基本平面(65536个字符)的字符,转义规则:\u,\u后跟上4位十六进制数:

console.log('\u00A9'); // ©
console.log('\u0075'); // u
console.log('\u597d'); // 好

对于非基本平面的字符,则要使用UTF-16的码元规则,一般是两个码元:\u\u,\u后各4位十六进制:

console.log('\uD83D\uDE04'); // 😄
console.log('\uD83C\uDF34'); // 🌴

只要获取到字符的Unicode编号码点,即可以通过这种方式转义所有的字符。

正则表达式中的转义

在正则表达式中有许多特殊的符号,起着不同的作用,但如果要匹配这些特殊符号本身,就需要用到转义了。 这里的转义字符也是使用反斜杠,后面跟上需要匹配的符号,即可,如:+ 匹配加号。 在正则表达式中,需要反斜杠转义匹配的字符,一共是12个:

^ . [ $ ( ) | * + ? { \,

这些字符都是正则的特殊字符。

console.log(/5$/.test('5$'));   // false
console.log(/5\$/.test('5$'));  // true

上面代码,$符号加了转义才能匹配成功,否则不匹配。 当使用RegExp方法时,正则参数是字符串,反斜杠\需要2个,因为字符串内会先转义一次:

console.log((new RegExp('5$')).test('5$'));   // false
console.log((new RegExp('5\$')).test('5$'));  // false
console.log((new RegExp('5\\$')).test('5$')); // true

上面代码中,只有第三行因为使用了两个反斜杠,才能转义成功,得到正确的值。

HTML中的转义

HTML中转义字符也是较常见的,主要有三种形式的转义。 最常见的是基于实体名称: & 后加上实体名称 再加上分号 ;: 空格
大于号 > > 实体名称转义以前是为了处理html中的特殊的字符显示问题,比如小于号 <在THML中是作为元素标签的一部分使用的,作为特殊字符,直接输入会出错。 但目前在现代浏览器上,已经能正常显示大于小于号这些符号,可以不使用转义字符。 另两种是基于Unicode码点: &#x 后加十六进制码点 再加上分号 ;:

<!-- 😄 好 & -->
&#x1f604; &#x597d; &#x26;

&# 后加十进制的码点 再加上分号 ;

<!-- 😄 好 & -->
&#128516; &#22909; &#38;

基于实体名称转义的字符,也是被包含在Unicode字符集中的,所以也可以使用码点来进行转义。 在HTML中最常用到的转义字符,可见下面整理的表格:

字符名称名称转义码点转义
"双引号""
&和号&&
<小于号<<
大于号>>
'单引号''
空格空格  
©版权号©©
¢¢
£££
¥人名币¥¥
$美元$$
©版权©©
®注册商标®®
×乘号××
÷除号÷÷

URL转义字符

另外,在web开发中,URL链接也是有编码转义的,特别是针对URL元字符和中文等特殊字符: baidu.com/中国 编码转义为 http%3A%2F%2Fbaidu.com%2F%E4%B8%AD%E5%9B%BD 可以使用encodeURIComponent 和 decodeURIComponent 对URL链接进行编解码处理。 URL中的转义规则:根据系统的默认编码(一般是UTF-8),是使用百分号(%)加上两位的十六进制数。 实际的转义,就是基于Unicode字符的码点,不过与字符串不太一样,这里使用的一般是UTF-8编码方式。 URL部分元字符的转义:

元字符名称转义符
+加号%2B
空格空格%20
/斜杠%2F
?问号%3F
#井号%23
&和号%26
=等号%3D
:冒号%3A
@at符%40
,逗号%2C
;分号%3B

中文等特殊字符则被转义成多个组合:

// 'http%3A%2F%2Fbaidu.com%2F%E4%B8%AD%E5%9B%BD%F0%9F%98%84'
encodeURIComponent('http://baidu.com/中国😄') 
  • 元字符的转义:':' => %3A、'/' => %2F,
  • 而中文则转义成:中 => %E4%B8%AD、国 => %E5%9B%BD,
  • 表情符号转义成:😄 => %F0%9F%98%84

字符集和字符编码

字符集就是字符的集合,如常见的 ASCII字符集,GB2312字符集,Unicode字符集等。这些不同字符集之间最大的区别是所包含的字符数量的不同。 字符编码则代表字符集的实际编码规则,是用于计算机解析字符的,如 GB2312,GBK,UTF-8 等。字符编码的本质就是如何使用二进制字节来表示字符的问题。 同一字符集可能有多种字符编码,如Unicode字符集就有 UTF-8,UTF-16 等。 在前端开发中,Javascript程序是使用Unicode字符集,Javascript源码文本通常是基于UTF-8编码。 但JS代码中的字符串类型是UTF-16编码的,这也是为什么会碰到api接口返回字符串在前端出现乱码,因为多数后台服务都使用utf-8编码,前后编码方式不一致。

字符集的发展历史

说起字符集的发展历程,可以总结为一句话:**ASCII是几乎所有字符集的基础,**几乎都是对ASCII字符集的扩展。

  1. 标准的ASCII码最多只能标识128个字符,欧美国家可以很好的使用,但其他国家的字符变多,自然就不够用了。这个时候,最高位就开始被惦记上,通过扩展ASCII码的最高位,又能满足用于特殊符号的一些国家的需求,这种就是扩展ASCII码。
  2. 但是亚非拉更多非拉丁语系的国家,字符成千上万,只能使用新的方式。如中文,就又进行了扩展,小于127的字符的意义与标准ASCII码相同,当需要标识汉字时,使用2个字节,每个字节都大于127。这种多字节字符集即GB2312,后续因为不断的扩展,如繁体字和各种符号,甚至少数民族的语言符号等等,又使用了包括GBK等不同字符集。
  3. 因此,很多国家都制定了自己的编码字符集,基本都是在ASCII的基础上进行的。各字符集虽然都能够兼容标准ASCII码,但在使用交流上的不便是显而易见的,乱码也是随处可见。为了解决这种各自为战的问题,**Unicode **字符集就诞生了。

ASCII 码 美标编码

image.png image.png

  • 1个字节有8个位,每个位可以表示0或1,1个字节可以表示256中状态
  • 对英语字符与二进制位之间的关系,做了统一规定
  • ASCII 码一共规定了128个字符的编码,用1个字节的后7位就够了,规定第一个位统一为0。英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。
function generateString(start, end) {
  let result = ""
  for (let i = start; i < end; i++) {
    result += String.fromCharCode(i);
  }
  return result
}

console.log(generateString(0, 255));
console.log(generateString(0x2E80, 0x9FFF)); //汉语区间 0x4e00-0x9fbb 或 0x2E80-0x9FFF
console.log(generateString(0x2E80, 0x9FFF)); 
/*
123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
*/



console.log(String.fromCharCode(2 ** 16 - 1));  //65535 - 1
console.log(String.fromCodePoint(2 ** 16 - 1)); //65535 - 1
console.log("🐯".charCodeAt());                 // 55357
console.log("🐯".codePointAt());                // 128047

/* 
通过ASCII码表,我们发现,小写字母并没有和大写字母挨着排序?
这是为了方便大小写之间的转换, A 排在 65(64 + 1) 位,而 a 排在 97(64 + 32 + 1) 位。
65 ^ 32 = 97
*/
console.log(String.fromCharCode("A".charCodeAt() ^ 32)); // a

Unicode字符集

Unicode 是国际组织制定的,用于收纳世界上所有文字和符号的字符集方案。 Unicode 的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS 前128个字符同ASCII一样,进行扩充后,使用数字0-0x10FFFF来映射这些字符,最多可以有1114112个字符。目前仍然只使用了其中的一小部分。 :::tips Unicode一般使用两个字节来表示一个字符。 码点

  • Unicode 规定了每个字符的数字编号,这个编号被称为 码点(code point)。
  • 码点以 U+hex 的形式表示,U+是代表Unicode的前缀,而 hex 是一个16进制数。取值范围是从 U+0000 到 U+10FFFF。
  • 每个码点对应一个字符,绝大部分的常见字符在最前面的 65536 个字符,范围是 U+0000到U+FFFF。
  • 一般汉字的码点区间为 U+2E80 - U+9FFF。

码元

  • 码元(Code Unit)可以理解为对码点进行编码时的最小基本单元,码元是一个整体。而字符编码的作用就是将Unicode码点转换成码元序列。
  • Unicode常用的编码方式有 UTF-8 、UTF-16 和 UTF-32,UTF是Unicode TransferFormat的缩写。
  • UTF-8是8位的单字节码元,UTF-16是16位的双字节码元,UTF-32是32位的四字节码元。

字符平面

  • 目前的Unicode分成了17个编组,也称平面,每个平面有65536个码点。
  • 第一个平面是基本多语言平面,范围:U+0000 - U+FFFF,多数常见字符都在该区间。
  • 其他平面则为辅助平面,范围:U+10000 到 U+10FFFF,如我们在网上常见 Emoji 表情。 ::: | 编码方式 | 码元 | 编码后字节数 | | --- | --- | --- | | UTF(UCS Transformation Format)规范,常见的UTF规范包括 UTF-7、UTF-8、UTF-16、UTF-32 | | | | UTF-8 | 8位 | 1-4字节 | | UTF-16 | 16位 | 2字节或者4字节 | | UTF-32 | 32位 | 4字节 | | | | | | UCS-2 | 16位 | 使用2个字节表示已经有码点的字符。 UCS-2只是一个编码方案,js起初使用的编码,后来用UTF-16代替 | | UCS-4 | 32位 | 4个字节(实际上只用了31位,最高位必须为0)编码 |

另外,为什么总看到使用十六进制数据来表示如码点等各种数据呢? 因为,两位的十六进制正好等于一个字节8位,0xff = 0b11111111。

UTF-8

以8位为单元对UCS进行编码,是一种可变长度的字符编码方式。目前是使用 1 到 4 个字节来编码字符。是互联网时代应用最广的一种编码方式,前端接触的相对最多。

  • 它是一种变长的编码方法,字符长度从1个字节到4个字节不等。
  • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。需要注意的是:汉字一般占3个字节,表情符号一般占4个字节。
  • 由于UTF-8这种节省空间的特性,导致它成为互联网上最常见的网页编码。
  • 将16进制的unicode码按照utf-8的存储规则生成二进制,可以再转换成16进制,unicode的16进制与utf-8的16进制表示不相等。

UTF-8的编码规则:

  • 1个字节的字符,第一位为0,后7位为码点,与ASCII相同。
  • n个字节的字符,第一个字节前面 n 位都是1,n+1位是0,可据此判断有几个字节。后面的几个字节都是 10 为开头2位。

这里规定的都是前缀,对于字符的码点,需要进行截取后依次放入除前缀外的其他位,所以UTF-8又被称为前缀码。格式如下表:

字节数码点位数码点范围编码方式
17U+0000~U+007F
0-1270×××××××
211U+0080~U+07FF
128-2047110××××× 10××××××
316U+0800~U+FFFF
2048-655351110×××× 10×××××× 10××××××
421U+10000~U+10FFFF
65535-111411111110××× 10×××××× 10×××××× 10××××××

通过上表的编码规则,我们就可以进行各种转换了。 下面我们以一个中文字符的编码转换为例,如汉字 '好':

console.log('好'.codePointAt())
/* 
22909
22909在UTF-8的3字节数的编码区间 U+0800 (2048) ~ U+FFFF (65535)
22909的二进制值:101100101111101,有15位
而3字节数的编码需要16位,前面补0,根据表中规则分成3组: 0101 100101 111101
依次填入对应的前缀: 11100101 10100101 10111101, 得到3个字节
将得到的三个字节转成十六进制数据:E5 A5 BD,所以汉字 '好' 的UTF-8就是:E5 A5 BD
*/

// 我们使用 encodeURI 进行验证————encodeURI函数支持将中文进行 UTF-8 编码:
// 去除百分号,结果正好一致。
console.log(encodeURI('好')) // '%E5%A5%BD'

UTF-16

  • UTF-16编码介于UTF-32与UTF-8之间。
  • UTF-16取代了UCS-2,或者说UCS-2整合进了UTF-16。所以,现在只有UTF-16,没有UCS-2。
  • UTF-16的编码方式:基本平面的字符占用 2 个字节(U+0000到U+FFFF),辅助平面的字符占用 4 个字节(U+010000到U+10FFFF)
  • 也就是说,UTF-16的编码长度要么是2个字节要么是4个字节。当为2字节时,则实际上是与Unicode相同。
  • 并且还有个原则,在Unicode基本多语言平面内,从U+D800到U+DFFF之间的码点区间是不对应字符的。而UTF-16需要利用这块码位来对辅助平面的字符进行编码。

它的具体规则: 码点小于U+FFFF,基本字符,不需处理,直接使用,占两个字节。 否则,拆分成两个码元,四个字节,cp表示码点:

  • 低位 ((cp - 65536) / 1024) + 0xD800,值范围是 0xD800~0xDBFF;
  • 高位 ((cp - 65536) % 1024) + 0xDC00,值范围是 0xDC00~0xDFFF。 看下面的示例:
console.log('好'.codePointAt())
console.log('好'.codePointAt().toString(16)); // 597d
/*  
22909
码点小于U+FFFF,直接进行十六进制转换:579D。
*/

// -------------------------------------------------------
let str = "😄"
console.log(str.codePointAt())  // 128516
console.log(str.charCodeAt())   // 55357
console.log(str.length)         // 2
let codeArr = []

let loworderCode = Math.floor(((128516 - 65536) / 1024)) + 0xD800;
let higorderCode = Math.floor(((128516 - 65536) % 1024)) + 0xDC00;
console.log(loworderCode, higorderCode); // 55357 56836

for (const key in str) {
  codeArr.push(str[key].charCodeAt())
}
console.log(codeArr)  // [ 55357, 56836 ]

console.log(String.fromCodePoint("128516"));                       // 😄
console.log(String.fromCharCode(...[loworderCode, higorderCode])); // 😄
console.log(String.fromCharCode(...codeArr));                      //😄

UTF-32

  • 每个码点使用四个字节表示,字节内容一一对应码点。
  • UTF-32的优点在于,转换规则简单直观,查找效率高。
  • 缺点在于浪费空间,同样内容的英语文本,它会比ASCII编码大四倍。

下面将选取字母、汉字、表情字符,进行编码对比查看

/* 
UTF-8
'a': 97 - 0x61
'好': 22909 - (0xE5 0xA5 0xBD)
'😄': 128516 - (0xF0 0x9F 0x98 0x84) 
*/
var log = console.log
log(Buffer.from('a', 'utf-8'))  // <Buffer 61>
log(Buffer.from('好', 'utf-8')) // <Buffer e5 a5 bd>
log(Buffer.from('😄', 'utf-8')) // <Buffer f0 9f 98 84>



/* 
UTF-16
'a': 97 - 0x0061
'好': 22909 - 0x597d
'😄': 128516 - (0xD83D, 0xDE04)
 */
var log = console.log
log(Buffer.from('a', 'utf-16le'))  // <Buffer 61 00>
log(Buffer.from('好', 'utf-16le')) // <Buffer 7d 59>
log(Buffer.from('😄', 'utf-16le')) // <Buffer 3d d8 04 de>

Hex 编码

将每一个字节表示的16进制表示的内容,用字符串来显示。

Base64 编码

  • 是一组相似的二进制到文本的编码规则
  • Base64编码普遍应用于需要通过被设计为处理文本数据的媒介上储存和传输二进制数据而需要编码该二进制数据的场景。
  • 在 JavaScript 中,有两个函数被分别用来处理解码和编码 base64 字符串:
    • atob() 函数解码base-64编码的字符串数据。
    • btoa() 函数能够从二进制数据“字符串”创建一个base-64编码的ASCII字符串。

GB2312-1980 国标编码

  • 使用双字节表示一个中文字符,可以支持256*256=65536个字符,
  • GB2312是GBK的一个子集。
  • 共收入汉字6763个和非汉字图形字符682个,相当于收录都是简体中文,比较少

GBK-1995 编码

  • GBK 采用双字节表示,兼容GB2312,但不兼容BIG5。
  • 共收录了21003个汉字

GB18030-2005 编码

  • 每个字符的编码可以是1、2或4个字节
  • 其对GB 2312-1980完全向后兼容,与GBK基本向后兼容,共收录汉字70,244个
  • 现在的PC平台必须支持GB18030,还收录了藏文、蒙文、维吾尔文等主要的少数民族文字
  • 强制性标准

CJK 编码

中日韩统一表意文字

BIG5-2003 编码

每个汉字由两个字节构成 是针对繁体汉字的汉字编码,台湾地区常用,共收录13,060个汉字

encodeURI() 编码 和 decodeURI() 解码

  • 该方法不会对ASCII表中的字母和数字编码,同时也不会对ASCII中的标点符号编码 -_.~*’() 在URI中具有特殊含义的符号 ;/?😡&=+$,#同样不会被编码。
  • 将特定字符的每个实例替换为一个、两个、三或四转义序列来对统一资源标识符 (URI) 进行编码 (该字符的 UTF-8 编码仅为四转义序列)由两个 "代理" 字符组成)。
  • decodeURI() 函数能解码由创建或其它流程得到的统一资源标识符(URI)。
var log = console.log
log(encodeURI('😄好 https://yuque.com/kenguba'))  
log(decodeURI('%F0%9F%98%84%E5%A5%BD%20https://yuque.com/kenguba'))  

encodeURIComponent() 编码 和 decodeURIComponent() 解码

  • 该方法相比encodeURI多编码URI中具有特殊含义的符号 ;/?😡&=+$,#
  • 对统一资源标识符(URI)的组成部分进行编码的方法。它使用一到四个转义序列来表示字符串中的每个字符的UTF-8编码(只有由两个Unicode代理区字符组成的字符才用四个转义字符编码)。
  • decodeURIComponent() 方法用于解码由方法或者其它类似方法编码的部分统一资源标识符(URI)。
var log = console.log
log(encodeURIComponent('😄好 https://yuque.com/kenguba'))
log(decodeURIComponent('%F0%9F%98%84%E5%A5%BD%20https%3A%2F%2Fyuque.com%2Fkenguba'))   

escape() 编码(已废弃) unescape() 解码(已废弃)

  • escape() 生成新的由十六进制转义序列替换的字符串
  • unescape() 计算生成一个新的字符串,其中的十六进制转义序列将被其表示的字符替换。
var log = console.log
log(escape('😄好 https://yuque.com/kenguba')) 
log(unescape('%uD83D%uDE04%u597D%20https%3A//yuque.com/kenguba'))   

encodeURI、encodeURIComponent、escape 区别

  • encodeURI() 把字符串编码为 URI
  • encodeURIComponent() 把字符串编码为 URI 组件
  • escape() 对字符串进行编码
var str = "http://localhost:8080/Product/index?id=123&attr=456&area=中国";
console.log(str);
console.log(encodeURI(str));
console.log(encodeURIComponent(str));
console.log(escape(str));

/*  
http://localhost:8080/Product/index?id=123&attr=456&area=中国
http://localhost:8080/Product/index?id=123&attr=456&area=%E4%B8%AD%E5%9B%BD
http%3A%2F%2Flocalhost%3A8080%2FProduct%2Findex%3Fid%3D123%26attr%3D456%26area%3D%E4%B8%AD%E5%9B%BD
http%3A//localhost%3A8080/Product/index%3Fid%3D123%26attr%3D456%26area%3D%u4E2D%u56FD
*/

:::tips **URL元字符:**分号(;),逗号(’,’),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(# :::

  • encodeURI不会对 URL元字符起分割作用的字符进行编码;encodeURIComponent则会。
  • 观察escape则发现,:?&都被转码了,而/没有,w3school解释是,escape函数会对ascii码中字母、数字及符号( * @ - _ + . / )之外的所有字符进行编码。
  • 另外,我们可以看出escape对汉字“中国”编码后结果与前两者不同。W3SCHOOL也建议不使用该方法,用前两者代替

前端开发中的编码

处理码点和字符

  • **charAt(index):**从一个字符串中返回指定的字符,对于多码元字符,却会返回码元字符:
var log = console.log

log('65'.charAt())        // '6'
log('a'.charAt())         // 'a'
console.log('😄'.length) //2
log('😄'.charAt())       // � ==='\uD83D'
log('😄'.charAt(1))      // � ==='\uDE04'
log('\uD83D\uDE04')      // 😄
  • charCodeAt(index):返回0到65535之间的整数码点值。对于多码元字符如果码点大于U+FFFF,则返回第一个码元值,还可以加索引参数取后面码元的值。
var log = console.log

log('65'.charCodeAt())        // 54
log('a'.charCodeAt())         // 97
log('好'.charCodeAt())        // 22909
console.log('😄'.length)     // 2
log('😄'.charCodeAt())       // 55357
log('😄'.charCodeAt(1))      //56836

log(String.fromCharCode(55357, 56836))          // 😄
  • **codePointAt(pos):**返回Unicode码点,多码元也能返回完整的码点值。codePointAt可以传入索引参数,对多码元字符取第二个码元值。
var log = console.log

log('65'.codePointAt())        // 54
log('a'.codePointAt())         // 97
log('好'.codePointAt())        // 22909
console.log('😄'.length)      // 2
log('😄'.codePointAt())       // 128516
log('😄'.codePointAt(1))      // 56836

log(String.fromCodePoint(128516))          // 😄
  • String.fromCharCode(num1[, ...[, numN]]):返回由指定的UTF-16码点序列创建的字符串。参数范围0到65535,大于65535的数据将被截断,结果不准确。对于多码元字符,则会将两个码元组合得到该字符。
  • **String.fromCodePoint(num1[, ...[, numN]]):**返回使用指定的代码点序列创建的字符串。可以处理多码元字符的完整码点值。
var log = console.log

log(String.fromCharCode(55357, 56836, 123, 65535))     // 😄{￿
log(String.fromCodePoint(128516, 123, 65535))          // 😄{￿

组合字符的长度

还有一种特殊的,组合字符,一般指一些带标点符号的字符:é。

var log = console.log

log('é'.length) // 2
log('e\u0301'.length) // 2

// 获取码点时,忽略了标点符号,显示的是字母的码点
log('é'.codePointAt()) // 101
log('e'.codePointAt()) // 101

// 如要正常操作组合字符,使用normalize()。
log('é'.normalize().length) //1

多码元字符操作

对于多码元字符使用下标取值时,得到的将是它的码元:

log('😄'[0], '😄'[0].charCodeAt(), '😄'[0].charCodeAt().toString(16))    // � 55357 d83d
log('😄'[1], '😄'[1].charCodeAt(), '😄'[1].charCodeAt().toString(16))    // � 56836 de04
log("\ud83d\ude04")   // 😄
log('123'[0])         // '1'

循环时,使用 for 会乱码,而 for-of 则正常:

let smile = '😄'
for (let i = 0; i < smile.length; i++) {
  console.log(smile[i])
  /*  
  �
  �
  */
}

for (let v of smile) {
  console.log(v) // 😄
}

但是,可以使用转换成扩展数组的方式访问:

var log = console.log

log([...'😄'][0])     // '😄'
log(Array.from('😄')) // ['😄']

还可以使用码点的方式

log('😄'.charCodeAt(0));  // 55357
log('😄'.codePointAt(0)); // 128516

log('😄'.charCodeAt(1));  // 56836
log('😄'.codePointAt(1)); // 56836

log('😄'.codePointAt(2)); // undefined

log(String.fromCodePoint('😄'.codePointAt())) // '😄'

对于这种特殊字符,使用下面的字符串方法都会分割码元:

log('😄'.slice(0, 2))  // '😄'
log('😄'.slice(0, 1))  // � === '\uD83D'
log('😄'.slice(1, 2))  // � === '\uDE04'
log('😄'.substr(0, 1)) // � === '\uD83D'
log('😄'.substr(0, 2)) // '😄'

log('😄'.split('')) // ['\uD83D', '\uDE04']

正则中的 u 修饰符

ES6在正则中添加了u修饰符,用来正确处理大于\uFFFF的 Unicode 字符。 也就是能够正确处理四个字节的 UTF-16 编码。

log(/^\S$/.test('😄')) // false
log(/^\S$/u.test('😄')) // true

但对组合字符,u修饰符不起作用:

log(/^\S$/u.test('é'))       // false
log(/^\S$/u.test('e\u0301')) // false

转义字符输出

我们还需要注意的,是转义字符的计算,结果会以实际字符为准:

log('\x3f'.length) // 1
log('?'.length)    // 1

读取操作时,也能正常处理:

log('\x3f'[0])        // '?'
log('\x3f'.split('')) // ['?']

TextEncoder

TextEncoder,使用 UTF-8 编码将代码点流转换成字节流。 TextDecoder:解码。 默认编码方式就是UTF-8,可以解决字符转UTF-8编码的问题。

var log = console.log

const enVal = new TextEncoder().encode('好')
log(enVal) // Uint8Array(3) [ 229, 165, 189 ]
log(new TextDecoder().decode(enVal))  // 好

⚠️**注意:**IE低版本不支持。

String.prototype.normalize()

对于语调符号和重音符号,Unicode提供了两种方法,一种是直接提供带符号的字符,如 é (码点233);另一种是组合字符,如上文提到的 é (码点101)。 针对这种码点不同,但实质一样的字符,Javascript识别不了: 而 normalize() 方法的引入,正是为了解决这一问题,它会按照一定的方式将字符的不同表示方法统一为标准形式:

log('é' === 'é')// false

log('é' === 'é'.normalize())// true

字符串长度计算

字符串的length属性,实际上是使用UTF-16的码元个数来进行计算的:

  • ASCII码和大部分中文,都是一个码元
  • 而表情字符和其他特殊字符都是两个码元

所以当某个字符中存在2个码元时,就算显示的是一个字符,length却等于2。

var log = console.log
log('a'.length)    // 1
log('好'.length)   // 1,多数汉字都是基本字符平面,只有一个码元,长度就为1。
log('😄'.length)  // 2

GBK、UTF8长度计算

// GBK字符集实际字节长度计算
function getStrLeng(str) {
  var realLength = 0;
  var len = str.length;
  var charCode = -1;
  for (var i = 0; i < len; i++) {
    charCode = str.charCodeAt(i);
    if (charCode >= 0 && charCode <= 128) {
      realLength += 1;
    } else {
      // 如果是中文则长度加2
      realLength += 2;
    }
  }
  return realLength;
}
// UTF8字符集实际字节长度计算
function getStrLeng(str) {
  var realLength = 0;
  var len = str.length;
  var charCode = -1;
  for (var i = 0; i < len; i++) {
    charCode = str.charCodeAt(i);
    if (charCode >= 0 && charCode <= 128) {
      realLength += 1;
    } else {
      // 如果是中文则长度加3
      realLength += 3;
    }
  }
  return realLength;
}

Javascript 获取字符串字节数的多种方法

第一种:(通过String对象的charCodeAt方法)

String.prototype.getBytesLength = function () {
  var length = 0;
  for (i = 0; i < this.length; i++) {
    iCode = this.charCodeAt(i);
    if ((iCode >= 0 && iCode <= 255) || (iCode >= 0xff61 && iCode <= 0xff9f)) {
      length += 1;
    } else {
      length += 2;
    }
  }
  return length;
}


let str = "🐯一缕清风abc123"
console.log(`'${str}'.length=${str.length}`)
console.log(str.getBytesLength());
/*  
'🐯一缕清风abc123'.length=12
18
*/

第二种:(通过escape()方法转编码后判断)

String.prototype.getBytesLength = function () {
  var str = escape(this);
  console.log(str)  // %uD83D%uDC2F%u4E00%u7F15%u6E05%u98CEabc123
  for (var i = 0, length = 0; i < str.length; i++, length++) {
    if (str.charAt(i) == "%") {
      if (str.charAt(++i) == "u") {
        i += 3;
        length++;
      }
      i++;
    }
  }
  return length;
}


let str = "🐯一缕清风abc123"
console.log(`'${str}'.length=${str.length}`)
console.log(str.getBytesLength());
/*  
'🐯一缕清风abc123'.length=12
18
*/

第三种写法:正则匹配,但是会修改原来的

String.prototype.getBytesLength = function () {
  return this.replace(/[^\x00-\xff]/gi, "--").length;
}


let str = "🐯一缕清风abc123"
console.log(`'${str}'.length=${str.length}`)
console.log(str.getBytesLength());
/*  
'🐯一缕清风abc123'.length=12
18
*/

JS字符串转GBK编码

第一种:使用TextEncoder

const str = '🐯 中文字符';

const utf8Array = new TextEncoder('gb2312').encode(str);
const utf8String = new TextDecoder('utf-8').decode(utf8Array);
console.log(utf8String);

const gb2312Array = new TextEncoder('utf-8').encode(str);
const gb2312String = new TextDecoder('gb2312').decode(gb2312Array);
console.log(gb2312String);

第二种:用Uint16Array收集对照表 image.png

<script>
  const ranges = [
    [0xA1, 0xA9, 0xA1, 0xFE],
    [0xB0, 0xF7, 0xA1, 0xFE],
    [0x81, 0xA0, 0x40, 0xFE],
    [0xAA, 0xFE, 0x40, 0xA0],
    [0xA8, 0xA9, 0x40, 0xA0],
    [0xAA, 0xAF, 0xA1, 0xFE],
    [0xF8, 0xFE, 0xA1, 0xFE],
    [0xA1, 0xA7, 0x40, 0xA0],
  ]

  const codes = new Uint16Array(23940)
  let i = 0
  for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
    for (let b2 = b2Begin; b2 <= b2End; b2++) {
      if (b2 !== 0x7F) {
        for (let b1 = b1Begin; b1 <= b1End; b1++) {
          codes[i++] = b2 << 8 | b1
        }
      }
    }
  }
  const str = new TextDecoder('GBK').decode(codes)

  // 编码表
  const table = new Uint16Array(65536)
  for (let i = 0; i < str.length; i++) {
    table[str.charCodeAt(i)] = codes[i]
  }

  function stringToGbk(str) {
    const buf = new Uint8Array(str.length * 2)
    let n = 0

    for (let i = 0; i < str.length; i++) {
      const code = str.charCodeAt(i)
      if (code < 0x80) {
        buf[n++] = code
      } else {
        const gbk = table[code]
        buf[n++] = gbk & 0xFF
        buf[n++] = gbk >> 8
      }
    }
    return buf.subarray(0, n)
  }

  console.log(stringToGbk('你好123'));    // [196, 227,186, 195, 49, 50, 51]
</script>

'use strict'

let table

function initGbkTable() {
  // https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
  const ranges = [
    [0xA1, 0xA9, 0xA1, 0xFE],
    [0xB0, 0xF7, 0xA1, 0xFE],
    [0x81, 0xA0, 0x40, 0xFE],
    [0xAA, 0xFE, 0x40, 0xA0],
    [0xA8, 0xA9, 0x40, 0xA0],
    [0xAA, 0xAF, 0xA1, 0xFE],
    [0xF8, 0xFE, 0xA1, 0xFE],
    [0xA1, 0xA7, 0x40, 0xA0],
  ]
  const codes = new Uint16Array(23940)
  let i = 0

  for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
    for (let b2 = b2Begin; b2 <= b2End; b2++) {
      if (b2 !== 0x7F) {
        for (let b1 = b1Begin; b1 <= b1End; b1++) {
          codes[i++] = b2 << 8 | b1
        }
      }
    }
  }
  table = new Uint16Array(65536)
  table.fill(0xFFFF)

  const str = new TextDecoder('gbk').decode(codes)
  for (let i = 0; i < str.length; i++) {
    table[str.charCodeAt(i)] = codes[i]
  }
}


const NodeJsBufAlloc = typeof Buffer === 'function' && Buffer.allocUnsafe

const defaultOnAlloc = NodeJsBufAlloc
  ? (len) => NodeJsBufAlloc(len)
  : (len) => new Uint8Array(len)

const defaultOnError = () => 63   // '?'


function str2gbk(str, opt = {}) {
  if (!table) {
    initGbkTable()
  }
  const onAlloc = opt.onAlloc || defaultOnAlloc
  const onError = opt.onError || defaultOnError

  const buf = onAlloc(str.length * 2)
  let n = 0

  for (let i = 0; i < str.length; i++) {
    const code = str.charCodeAt(i)
    if (code < 0x80) {
      buf[n++] = code
      continue
    }
    const gbk = table[code]

    if (gbk !== 0xFFFF) {
      buf[n++] = gbk
      buf[n++] = gbk >> 8
    } else if (code === 8364) {
      // 8364 == '€'.charCodeAt(0)
      // Code Page 936 has a single-byte euro sign at 0x80
      buf[n++] = 0x80
    } else {
      const ret = onError(i, str)
      if (ret === -1) {
        break
      }
      if (ret > 0xFF) {
        buf[n++] = ret
        buf[n++] = ret >> 8
      } else {
        buf[n++] = ret
      }
    }
  }
  return buf.subarray(0, n)
}


function cmpBuf(got, exp) {
  if (got.length !== exp.length) {
    throw 'length not equal'
  }
  for (let i = 0; i < got.length; i++) {
    if (got[i] !== exp[i]) {
      throw `element not equal. got[${i}]: ${got[i]} exp[${i}]: ${exp[i]}`
    }
  }
}

function check(str) {
  const gbkBuf = str2gbk(str)
  const str2 = new TextDecoder('gbk').decode(gbkBuf)
  if (str !== str2) {
    throw `incorrect: "${str}"(${str.length}) "${str2}"(${str2.length})`
  }
}

check('你好123')
check('€')
check('')

cmpBuf(
  str2gbk('123©456©'),
  [49, 50, 51, 63, 52, 53, 54, 63]
)

cmpBuf(
  str2gbk('123©456©', {
    onError: () => 32
  }),
  [49, 50, 51, 32, 52, 53, 54, 32]
)

cmpBuf(
  str2gbk('123©456©', {
    onError: () => 0xC1A1
  }),
  [49, 50, 51, 0xA1, 0xC1, 52, 53, 54, 0xA1, 0xC1]
)

cmpBuf(
  str2gbk('123©456', {
    onError: () => -1
  }),
  [49, 50, 51]
)

const invalidChars = []
const invalidStr = '123©456©'

str2gbk(invalidStr, {
  onError: (index, input) => {
    invalidChars.push({ index, input })
  }
})
console.assert(invalidChars.length === 2)
console.assert(invalidChars[0].index === 3)
console.assert(invalidChars[0].input === invalidStr)
console.assert(invalidChars[1].index === 7)
console.assert(invalidChars[1].input === invalidStr)


const shouldBuffer = str2gbk('赢', {
  onAlloc: (len) => Buffer.allocUnsafe(len)
})
console.assert(shouldBuffer instanceof Buffer)


const shouldUint8Array = str2gbk('赢', {
  onAlloc: (len) => new Uint8Array(len)
})
console.assert(shouldUint8Array instanceof Uint8Array)


const targetBuf = new Uint8Array(1000)
const gbkBuf = str2gbk('一二三四五六七八九十', {
  onAlloc: () => targetBuf.subarray(500)
})
cmpBuf(gbkBuf, targetBuf.subarray(500, 520))

jsbin.com/yokaqixeme/…

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>JS String To GBK</title>
    <style>
      body, input { font-family: monospace; } input { width: 400px; }
    </style>
  </head>
  <body>
    <div>JS String: <input type="text" id="txtStr" value="一缕清风"></div>
    <div>GBK (Dec): <input type="text" id="txtGbkDec" readonly></div>
    <div>GBK (Hex): <input type="text" id="txtGbkHex" readonly></div>
    <div><button id="btnConvert">Convert</button></div>
    <script type="module">
      import str2gbk from 'https://unpkg.com/str2gbk/index.js'
      btnConvert.onclick = function() {
        const gbkBuf = str2gbk(txtStr.value)
        txtGbkDec.value = gbkBuf
        txtGbkHex.value = [...gbkBuf]
          .map(v => v.toString(16).padStart(2, '0'))
          .join(' ')
      }
      btnConvert.onclick()
    </script>
  </body>
</html>

截取字符码元和码点

字符串截取截取得是码元,汉字有一些偏僻字占据两个码元,字符的函数往往不是我们期待

const str = '你是一个𠮷娃娃😻的小可爱'
console.log('😻'.length) //2
console.log(str.slice(5, 9)) //�娃娃�


String.prototype.sliceByPoint = function (pStart, pEnd) {
  let result = '' // 截取的结果
  let pIndex = 0  // 码点的指针
  let cIndex = 0  // 码元的指针
  while (true) {
    if (pIndex >= pEnd || cIndex >= this.length) { break }
    // point > 0xffff(65535) 大小决定是否占两个码元,
    // codePointAt函数用于检索字符串中特定位置的字符的Unicode代码点
    const point = this.codePointAt(cIndex)
    if (pIndex >= pStart) {
      result += String.fromCodePoint(point)
    }
    pIndex++
    cIndex += point > 0xffff ? 2 : 1
  }
  return result
}
console.log(str.sliceByPoint(5, 9)) //娃娃😻的

String.prototype.sliceByPoint2 = function (pStart, pEnd) {
  let result = '' // 截取的结果
  let index = 0  // 码点的指针
  for (const k of this) {
    if (index > pEnd) { return result }
    if (index >= pStart && index < pEnd) {
      result += k;
    }
    ++index
  }
  return result
}
console.log(str.sliceByPoint2(5, 9)) //娃娃😻的

参考文献

:::info JavaScript 转义和编码 一文带你搞懂JavaScript中转义字符的使用 JavaScript常见编码解码 JavaScript开发中需要搞懂的字符编码 :::