JavaScript 中的编解码问题
JavaScript 提供了众多编解码方法,比如
escape
,encodeURI
,btoa
,charCodeAt
,codePointAt
等等。今天我们就来详细的捋一捋这些方法的异同和使用场景。
说到编解码不得不提到大名顶顶的 UTF-8
,这个想必每个前端都不会陌生,因为我们的 HTML 文件的 head 标签内第一行往往是这么写的:<meta charset="UTF-8">
。
这里就不详细讲解 UTF-8
的具体编码方式了,但为了方便讲解上面提到的几个方法,常见的几种编码方式之间的关系还是要简单的提一下的。
大家都知道,无论是文本,图片,视频还是可以执行的游戏在计算机看来都是由 0
和 1
组成的。在存储层面,计算机并不关心你是何种格式的文件,也不 care 你是用什么编码方式进行存储,所有格式的编码方式不过是人类为了方便统一协作而约定俗成罢了。
注意,上面提到了编码这个概念。计算机最小的存储单位是比特(bit),一个比特要么是 0,要么是 1。因为一个比特对于人类来说能表示的范围太小了,所以约定用 8 个比特来代表一个字节(byte)。计算机最早是美国人发明的,所以最早的编码方式 ASCII
也是根据英语量身定做的。早期的美国人只需要存储 a-zA-Z0-9
以及其他常见的标点符号,加起来都没超过 128 个,用一个字节表示(一个字节有 8 bit,存储的信息量是 2^8 = 256)绰绰有余。
后来随着互联网的发展,越来越多的国家参与进来,但如何编码这些国家的文字却是个大问题。因此各个国家都推出了自己的编码方案,比如中国的 GB2312
和 GBK
。显然这种方式严重影响了不同语种的人群的交流,于是全球为了统一编码共同制定了一个新的编码标准:Unicode
。Unicode
收录了几乎所有已知的字符,为了保持兼容,前 256 个字符和 ASCII
表示的含义相同。
值得注意的是,Unicode
只是给字符分配了统一的编号而已,却没有规定如何存储这些编号,于是又衍生出了不同的编码方式,比较出名的有 UTF-8
,UTF-16
和 UTF-32
。这三种编码的细节就不在这里罗嗦了,想了解的可以直接查阅 WIKI。
不过不了解细节也没关系,需要知道的是:
UTF-8
对一个字符编码时使用 1 - 4 个字节不等;UTF-16
对一个字符编码时使用 2 个或 4 个字节;UTF-32
对一个字符编码时使用 4 个字节(所以这种编码方式很少使用)。
总的而言,UTF-8
在存储空间上有不小的优势,因此被使用的越来越广泛(当然也不尽然,对于纯中文来说,UTF-16 也许会更好也说不准,但谁让计算机早期一直是欧美引领着呢,他们使用的字符用 UTF-8 效果更好)。
科普结束,下面进入正文。
JavaScript 中如何获取一个字符的 Unicode 码值?
在 ES6 之前我们只有一个方法那就是 String.prototype.charCodeAt
,我们直接看下 MDN 是如何介绍这个方法的:
charCodeAt() 方法返回 0 到 65535 之间的整数,表示给定索引处的 UTF-16 代码单元
UTF-16 编码单元匹配能用一个 UTF-16 编码单元表示的 Unicode 码点。如果 Unicode 码点不能用一个 UTF-16 编码单元表示(因为它的值大于0xFFFF),则所返回的编码单元会是这个码点代理对的第一个编码单元) 。
有点懵,对不对。这个介绍确实让人一言难尽,听我白话文给你解释一下。
charCodeAt
返回给定索引处字符在 UTF-16 编码下的 Unicode 码值。为什么要说是在 UTF-16 编码下呢,为什么不直接说是 Unicode 码值呢。这是因为该函数只能返回 0 到 65535 之间的整数,而我们之前说过,UTF-16 使用 2 个或 4 个字节来编码字符,码值为 0 - 65535 之间的字符使用 UTF-16 编码正是使用 2 个字节的情况。换句话说凡是 Unicode 码值大于 65535 的字符,其 UTF-16 的编码必定使用 4 个字节,也就是说此时 charCodeAt
只能返回前两个字节,比如大家熟知的 emoji 表情:
'🀄'.charCodeAt() // 55356
而 🀄 真实的 Unicode 码值是:126980
大家可能又会有疑问了,那 charCodeAt
为啥不返回完整的 Unicode 码值呢,搞这一出是闹哪样?不是不想,而是没赶上。JavaScript 第一版被开发出来时还没有 UTF-16 编码,它使用的是一个叫做 UCS-2 的编码方式,这种编码方式就只能表示这么多的字符,并且在一段时间内 UCS-2 和 UTF-16 表示的是同一种编码,只是后来 UTF-16 又扩充了定义,成为了 UCS-2 的超集,才造成当下的尴尬局面。
好在,ES6 引进了新的方法能够获取完整的 Unicode 码值了,那就是 codePointAt
:
'🀄'.codePointAt() // 126980
示例代码都没有给方法传 index 参数,是因为不传参时,方法内部会默认当成传 0 处理。
JavaScript 中如何对 URL 进行编码
凡是使用 JavaScript 发送过异步请求的前端应该都知道需要对 URL 进行编码,也大概率都知道应该使用 encodeURI
或者 encodeURIComponent
来编码。这里我也不详细解释两者的区别了,我们来分析下为什么要对 URL 编码。
我们先来看下 URL 是如何定义的:
[协议类型]://[访问资源需要的凭证信息]@[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID]
对于一个合法的 URL, : / @ ? # =
等字符都是具有特定分隔符的作用,为了避免歧义,其他的填充片段理应进行编码。另外 URL 还规定其必须由可打印的 ASCII 字符构成。因此我们看到,encodeURI
对除了 ; , / ? : @ & = + $ A-Z a-z 0-9 - _ . ! ~ * ' ( ) #
之外的所有字符都会进行转义编码;而 encodeURIComponent
则对除了 A-Z a-z 0-9 - _ . ! ~ * ' ( )
之外的所有字符进行转义编码。而编码的方式也很简单,那就是使用字符的 UTF-8 编码来表示。比如 中
的 UTF-8 使用 3 个字节表示 :0xE4 0xB8 0xAD
,其对应的 URL 编码则为 %E4%B8%AD
。
JavaScript 中如何获取一个字符的 UTF-8 编码
我们刚刚提到,encodeURIComponent
会对字符进行 UTF-8 编码,但可惜不是所有字符。不过莫慌,JavaScript 提供了更加强大的编码方法:TextEncoder
:
const encoder = new TextEncoder();
encoder.encode('中'); // Uint8Array(3) [228, 184, 173]
JavaScript 中如何进行 Base64 编码
这个就比较简单了: btoa
。这个函数容易和对应的解码函数:atob
搞混,其实也不难记,这里的 b 代表 binary,a 代表 ASCII。我们的变量(JavaScript 字符串)在内存中都是二进制形式保存的,所以是 binary,而 Base64 都是可打印的 ASCII 字符。所以 btoa 就是编码,而 atob 则是解码。