JavaScript 中的编解码问题

JavaScript 提供了众多编解码方法,比如 escapeencodeURIbtoacharCodeAtcodePointAt 等等。今天我们就来详细的捋一捋这些方法的异同和使用场景。

说到编解码不得不提到大名顶顶的 UTF-8,这个想必每个前端都不会陌生,因为我们的 HTML 文件的 head 标签内第一行往往是这么写的:<meta charset="UTF-8">

这里就不详细讲解 UTF-8 的具体编码方式了,但为了方便讲解上面提到的几个方法,常见的几种编码方式之间的关系还是要简单的提一下的。

大家都知道,无论是文本,图片,视频还是可以执行的游戏在计算机看来都是由 01 组成的。在存储层面,计算机并不关心你是何种格式的文件,也不 care 你是用什么编码方式进行存储,所有格式的编码方式不过是人类为了方便统一协作而约定俗成罢了。

注意,上面提到了编码这个概念。计算机最小的存储单位是比特(bit),一个比特要么是 0,要么是 1。因为一个比特对于人类来说能表示的范围太小了,所以约定用 8 个比特来代表一个字节(byte)。计算机最早是美国人发明的,所以最早的编码方式 ASCII 也是根据英语量身定做的。早期的美国人只需要存储 a-zA-Z0-9 以及其他常见的标点符号,加起来都没超过 128 个,用一个字节表示(一个字节有 8 bit,存储的信息量是 2^8 = 256)绰绰有余。

后来随着互联网的发展,越来越多的国家参与进来,但如何编码这些国家的文字却是个大问题。因此各个国家都推出了自己的编码方案,比如中国的 GB2312GBK。显然这种方式严重影响了不同语种的人群的交流,于是全球为了统一编码共同制定了一个新的编码标准:UnicodeUnicode 收录了几乎所有已知的字符,为了保持兼容,前 256 个字符和 ASCII 表示的含义相同。

值得注意的是,Unicode 只是给字符分配了统一的编号而已,却没有规定如何存储这些编号,于是又衍生出了不同的编码方式,比较出名的有 UTF-8UTF-16UTF-32。这三种编码的细节就不在这里罗嗦了,想了解的可以直接查阅 WIKI。

不过不了解细节也没关系,需要知道的是:

  1. UTF-8 对一个字符编码时使用 1 - 4 个字节不等;
  2. UTF-16 对一个字符编码时使用 2 个或 4 个字节;
  3. 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 则是解码。