在 Javascript 中处理编码问题有时会令人烦恼。我最近一次遭遇就是在使用浏览器的 atob
和 btoa
函数时。这些函数用于二进制字符串和 Base64 ASCII 码之间的转换。但是用它们处理 Unicode 字符串时就挂了:
> btoa('\u0227');
Error: INVALID_CHARACTER_ERR: DOM
Exception 5
MDN 上关于 btoa
的文档指出下面来自于 Johan Sundström 的函数可以解决 Unicode 问题:
function encode_utf8(s) {
return unescape(endcodeURIComponent(s));
}
function decode_utf8(s) {
return decodeURIComponent(escape(s));
}
用上这些函数后,btoa
就能够给出期望的结果了:
> btoa(encode_utf8('\u0227'));
"yKc=="
Johan 的函数能正常工作,只是让人觉得有点像是黑魔法。所以我决定研究一下 encode_utf8
函数的各个部分,搞明白它是如何工作的。
encode_utf8
函数中先使用了 encodeURIComponent
。encodeURIComponent
在最近的 ECMAScript 规范中定义如下:
encodeURIComponent 函数计算产生一个新的 URI ,其中的某些字符被替换为一个,二个,三个,或四个字节的 该字符 UTF-8 编码的转义序列。
令我惊讶的是 encodeURIComponent
函数是与 UTF-8 编码绑定的。而不像其它语言的类似函数,编码方式是作为函数参数传入的。如果你本来就像使用 UTF-8 编码,当然没有问题,如果你想使用别的编码方式就不好使了(当然也有别的方法使用 ArrayBuffers 实现定长编码)。
这儿有个例子是对 Unicode 字符 U+0227 (ȧ,小写的拉丁字母 A 顶上加个点)使用 encodeURIComponent
函数:
> encodeURIComponent('\u0227');
"%C8%A7"
结果是一个百分号编码的字符串,每一段表示该字符串 UTF-8 编码的一个字节。查找 Unicode 字符 U+0227 的文档,我们可以验证该字符 UTF-8 编码的十六进制形式就是 0xC8 0xA7 。
ECMAScript 的定义很模糊,它说 encodeURIComponent
会替换“某些字符”,却没说明这些字符。不过一个 快速的试验显示,任何大于 0x7E 的字符都会被编码,所以我觉得说任何非 ASCII 字符都会被 encocodeURIComponent
编码肯定不会错。由于百分号编码方式只使用了数字 0-9 ,字母 A-F 以及 ‘%’ 字符,可以保证输出结果一定是 ASCII 字符串。
现在我们可以先暂停一下。btoa
函数所需要的就是 ASCII 字符串,encodeURIComponent
函数就已然够用了:
> btoa(encodeURIComponent('\u0227'));
"JUM4JUE3"
不过这样做有一些缺陷。首先,输出的结果字符串与输入的字符串的二进制变了。这只是个原始字节的一个编码版本。这可能在与其它系统交互时造成困难和烦恼。
第二个缺陷是,encodeURIComponent
函数生成了一个更长的字符串。单个 Unicode 字符采用 UTF-8 编码最多占用 4 个字节,经过 URI 编码后就能产生一个 12 个字符长的串。之后在经过 Base64 编码(能使字符串变得更长),最终输出的结果比输入的字符能长许多倍。为了解决字符串长度问题, Johan 找来了 unescape
函数。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.