首页 > 技术文章 > 关于encodeURIComponent编码非UTF-8字符时出现的怪异情况

ryzz 2020-03-31 17:04 原文

现在有个test.html文件,这个文件的编码是UTF-8,其中“你好”的UTF-8编码是:E4 BD A0 E5 A5 BD,文件代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
	console.log(encodeURIComponent("你好")); //%E4%BD%A0%E5%A5%BD
</script>
</head>
<body>
	<p>hello</p>
</body>
</html>

可以看见,enencodeURIComponent就是将“你好”以UTF-8编码输出(这也正是encodeURIComponent方法的定义:将非URI字符都以UTF-8编码的格式输出为字符串),现在来看一个怪异的情况,还是上面的test.html(所以文件的编码依旧是UTF-8),只不过这次手动把charset改为了GBK编码,用来误导encodeURIComponent方法,代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="GBK">
<script>
	console.log(encodeURIComponent("你好")); //%E6%B5%A3%E7%8A%B2%E3%82%BD
</script>
</head>
<body>
	<p>hello</p>
</body>
</html>

什么,输出的是什么鬼!不着急,我们慢慢分析:
encodeURIComponent认为需要encode的字符的编码是charset指定的编码,这里就是GBK,而encodeURIComponent需要的是UTF-8编码的字符,这样它才会进行encode,所以必须发生编码之间的转换,具体如下,
“你好”的UTF-8:E4 BD A0 E5 A5 BD
所以“你好”实际上是以上面的6字节存储在文件test.html里面(因为文件的实际编码是UTF-8)
但是charset指定的GBK是2字节编码(除128个ASCII外,都是2字节编码),所以它把E4 BD认为是1个GBK字符,尝试把它转为UTF-8,其中E4 BD在GBK中对应的是“浣”字,而这个字在UTF-8则是E6 B5 A3
由此问题解决!

结论:如果网页文件的文件编码是UTF-8,而charset不小心指定成了其他的编码,那么会发生编码转换,第一次是将原本以UTF-8编码保存的字符当作charset指定的字符来读取,然后再把它转为UTF-8编码。

再来看一个更加离奇的情况,现在有个test2.html文件,它的文件编码是GBK,代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
	console.log(encodeURIComponent("你")); //%EF%BF%BD%EF%BF%BD
	console.log(encodeURIComponent("你你")); //%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD
	console.log(encodeURIComponent("你你你")); //%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD
</script>
</head>
<body>
	<p>hello</p>
</body>
</html>

“你”的GBK编码是C4E3,UTF-8的EFBFDB表示这些字节信息无法转为UTF-8对应的字符,现在我们来分析这个异常情况出现的本质原因:

C4的二进制是1100 0100,查看UTF-8编码格式转换表(百度百科的UTF-8词条内就有这个表格),确实存在以110开头的格式,以110开头的字节会和它的下个字节组合为一个字符,而下个E3的二进制是11100 011,而UTF-8第二个字节开始都是10开始,显然这时不匹配了,也就是说C4E3这个编码是非法的UTF-8编码,那么就会返回EFBFDB,由此问题解决。

 对于文件本身是GBK编码(或者其他非UTF-8编码),且charset指定的编码不是文件实际的编码,那么这个方法会出现很多意料之外的行为,再比如,如下代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
	console.log(encodeURIComponent("小")); //%D0%A1
	console.log(encodeURIComponent("小小")); //%D0%A1%D0%A1
	console.log(encodeURIComponent("小小小")); //%D0%A1%D0%A1%D0%A1
</script>
</head>
<body>
	<p>hello</p>
</body>
</html>

“小”的GBK编码是D0A1,同理,D0的二进制是1101 0000,也存在以110开头的UTF-8编码,下个A1的二进制是1010 0001,和上个“你”字不同,这个确实是符合UTF-8第二字节均以10开头的标准,那么这个字符是可以转为UTF-8的,由此可以正常输出。

最后再来看一个案例,文件依旧是GBK编码,代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
	console.log(encodeURIComponent("高")); //%EF%BF%BD%EF%BF%BD
	console.log(encodeURIComponent("高高")); //%EF%BF%BD%DF%B8%EF%BF%BD
	console.log(encodeURIComponent("高高高")); //%EF%BF%BD%DF%B8%DF%B8%EF%BF%BD
</script>
</head>
<body>
	<p>hello</p>
</body>
</html>

“高”的GBK编码是B8DF,而B8的二进制是1011 1000,呀,这个字节对于不了任何UTF-8起始字节,随即返回EFBFBD编码,示意这个字符无法对应(转换)为UTF-8编码,继续看下个DF的二进制,它是1101 1111,110可以对应,而UTF-8中起始字节是110会和它的下个字节组合为一个字符,对于只有一个“高”来说,没有下个字符了,单个的起始字符是不对应任何的UTF-8编码,随即也输出EFBFBD,这就是console.log第一行输出的结果,来看两个“高”,刚才讲到DF没有下个字符了,所以出错,现在有了第二个字符,是B8,那么就能组合了,因为B0的二进制以10开头是符合的,剩下的字节同理啦。

总结:通常来说,网页都采用UTF-8编码,即文件编码和charset指定的编码要相同,enencodeURIComponent才能封装和解析成功。

其实,只要文件编码和给出的charset编码相同,enencodeURIComponent就能正常工作,输出为UTF-8字符序列。

推荐阅读