JS基础-字符-Unicode编码(下)

这一篇阐述前端与编码相关的内容

一、HTML中的meta标签

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> </head> <body></body> </html>

这个标签对于前端来说太熟悉了,也都知道这个表示让浏览器使用UTF-8解码文档。但真总是这样吗?

1.Content-Type

其实浏览器获取页面的本身也是发送请求,如果是请求服务器大概率都会返回Content-Type这个属性,里面就包含了编码类型,那么浏览器会根据这个编码进行解码。并且忽视文档本身的,因为 在浏览器解码操作中,Content-Type 优先级大于<meta charset="xxx">

image
2. <meta charset="xxx">

如果服务器没有返回Content-Type这个属性,那浏览器只能找 <meta charset="xxx">,但是如果浏览器是根据 <meta charset="utf-8">来解析文本的话,那么ta本身也被编码了啊,浏览器是如何获取"utf-8" 的呢?是不是很奇怪。

这样的,因为大部分的主流编码都兼容了ASCII,并且我们的HTML文件前几行内容都是字母、符号、数字,所以浏览器可以直接吧body标签之前的部分按照ASCII解码,直到解码到时,得到xxx,可以确定编码方式了,然后再从文档开始部分,(一般为)进行整个文档的解码。

3. 没有Content-Type,也没有<meta charset="xxx">

如果服务端既没有返回Content-Type,文档内部也没有标明<meta charset="xxx">,这时候浏览器就真的是瞎猜了,他会根据一系类的算法进行匹配,以找出编码方式,因为每个编码都有自己的编码特色吗,但是你这个过程难免出错,尤其是你的文档并不是国际通用规范的编码时候。所有,我们还是老老实实把 <meta charset="UTF-8">定义出来吧,而且最好排在head标签中第一个位置。以便浏览器在没有Content-Type情况下尽快检索出编码方式。就像这样

html
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head>

二、script标签中的charset属性

由于现代浏览器纠错功能太强,所以,只要你的JS文件是使用UTF-8编码的,并且浏览器解析文档也是UTF-8的,无论charset属性怎么设置,都能正常解析。

MDN推荐我们:最好就是不去定义这个属性,因为他会继承HTML文档的编码格式。

W3school告诉我们:默认的字符编码是 ISO-8859-1。

这个问题不重要,不要过分纠结。

三、javascript的编码到底是 UCS-2 还是 UTF-16

这个问题问的在准确一点,应该是
javascript的字符串的存储操作,存到内存的编码到底是 UCS-2 还是 UTF-16

可以说,在js中,字符集编码是针对字符串这种数据结构的。

javascript到底是 UCS-2 还是
UTF-16 这个问题,我查阅很多资料说法不一,不过我个人认为最靠谱是:

在老版浏览器中,只支持UCS-2。当UTF-16发布出来后,根据浏览器对JS解释器的实现不同,可能使用 UCS-2,也有可能使用UTF-16。

现代浏览器(支持ES6的)全部都是UTF-16。

可以在ECMAScript的历史版本文档中,找到蛛丝马迹。

8793674-1795bb9a7c495397

四、javascript的解释器的

存储到内存的编码为UTF-16,我们保存的javascript文件为UTF-8,不会造成乱码吗?

不会,这个过程是,浏览器拿到服务器的HTML文件,这个文件是一堆字符串,浏览器把这些字符串解析成DOM节点,当解析到script标签时,把script的文件内容移交给JS编译器单元,对于编译器来说,这些文件内容也是一堆字符串而已,不然也就不会有词法和语法分析了,词法和语法分析的任务就是把这些javascript文件内的字符编译成解释器能执行的代码,然后在解释器执行

代码时,数据才以UTF-16编码格式进行存储,或者被取出操作取出数据。

总之,这个过程对于开发者是不可见的,所以我们一般不会在意。

我们能感知的部分就是字符串的相关操作,比如length属性,2字节编码的返回1,4字节编码的返回2。

明明显示的是一个字符,长度怎么还分成1和2呢?这说不通吗,所以很多初学者对于这个现象很是费解。不单是length属性,其他字符相关的方法也存在问题

js
// Unicode编码为 16进制:1f60d;10进制:128525 let str = "😍" console.log(str.length) // 2 console.log(str.charCodeAt()) // 55357 console.log(String.fromCharCode(128525)) // "" for(let i = 0;i<str.length;i++){ console.log(str.charAt(i)) //�� } console.log("😍".split('')) // ["�", "�"]

一个都不对,不过ES6已经为我们提供了替代方法了

js
let str = "😍" console.log(Array.from(str).length) // 1 console.log(str.codePointAt()) // 128525 console.log(String.fromCodePoint(128525)) // "😍" for (let s of str ){ console.log(s) //"😍" }

不过ES6并没有定义与charAt()方法对应的针对辅助平面的方法,好在有替补方案

js
let str = "😍" for (let i = 0; i < Array.from(str).length; i++) { console.log(fixedCharAt(str, i)) //"😍" } function fixedCharAt(str, idx) { var ret = ''; str += ''; var end = str.length; var surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; while ((surrogatePairs.exec(str)) != null) { var li = surrogatePairs.lastIndex; if (li - 2 < idx) { idx++; } else { break; } } if (idx >= end || idx < 0) { return ''; } ret += str.charAt(idx); if (/[\uD800-\uDBFF]/.test(ret) && /[\uDC00-\uDFFF]/.test(str.charAt(idx + 1))) { // Go one further, since one of the "characters" is part of a surrogate pair ret += str.charAt(idx + 1); } return ret; }

或者使用

bash
npm i string.prototype.at

js为什么不用UTF-8编码?

我个人认为,UTF-8作为大势所趋,不是想不想的问题,而是必须要改,今天我把话放这,以后再来挖坟。
但是现在还不能,原因以下几点:

1. 历史包袱

这是最主要的原因。js从UCS-2到UTF-16的过渡比较平滑,比如之间按照UCS-2编码的老网站,依然正常运行,或者js代码会使用length去判断字符串的长度,使用UTF-16之后,这些代码也不需要改动,返回值和之前一

样,因为已经说过UTF-16是UCS-2的超级,UTF-16和UCS-2的关系就像是ES6和ES5的关系一样,

在ES6中使用ES5完全没问题。所以浏览器的解释器使用UTF-16替换UCS-2的影响不是很大。
改成UTF-8呢?再拿字符串length这个属性为例,UTF-16编码可以直接2字节返回1,4字节返回2,你用了UTF-8如果还要这样返回,js解释器该怎么修改才合适?
ES6的关于字符串的修改其实就是在下一步大棋,那就是为JavaScript改为UTF-8编码做准备,因为好多属性都是根据字符本身返回长度,而不是字节数,这就少了很多顾虑。就看ES6什么时候能普及。

2. UTF-16更适合数据存储

因为相比UTF-8, 它有利于CPU的操作,有利于计算,如排序和索引,但是在一个文件基本都是ASCII
码表内容时,UTF-8比UTF-16更节省空间。但是相比这点空间,还是CPU的操作、计算、排序和索引重要一些。

3. 就是浏览器的生产商乐不乐意改

虽然我不太了解js解释器的内部架构是啥样的,但是我敢肯定,修改编码方式对于浏览器本身来说绝对
是伤筋动骨的操作,ECMA委员会成员一大部分都来自这些浏览器厂商,让他们束手就擒应该不是短期能办到的事。

五、前端页面有时候乱码是怎么回事?

修改一下当前文件的编码方式(VSCode编辑器)

8793674-70fa4889a7e45c54

之所以乱码,是因为我设置当前文件的编码为GBK,但是浏览器根据

8793674-c7cdd6d9e0318be5

的值,使用utf-8进行了解码,所以就是乱码。
总之乱码就是你编码用的A,你解码却用了B,那么A和B的交集部分能正常显示,交集之外的就是乱码。找了一张乱码对照表,但是现在由于UTF-8的
普及,很少会有乱码的出现了。

8793674-bdd1a701849b2869

锟斤拷乱码出现原因解析

源于 GBK 字符集和 Unicode 字符集之间的转换问题。Unicode 和老编码体系的转化过程中,肯定有一些字,用 Unicode 是没法表示的,Unicode 官方用了一个占位符来表示这些文字,这就是:U+FFFD REPLACEMENT CHARACTER。那么 U+FFFD 的 UTF-8 编码出来,恰好是 \xef\xbf\xbd。如果这个 \xef\xbf\xbd,重复多次,例如 \xef\xbf\xbd\xef\xbf\xbd,然后放到 GBK/CP936/GB2312/GB18030 的环境中显示的话,一个汉字 2 个字节,最终的结果就是:锟斤拷——锟(0xEFBF),斤(0xBDEF),拷(0xBFBD)。
出自https://moechu.cn/2020/03/16/405.html

其他:
如果你的html文件中包含汉字,服务器没有返回Content-Type这个属性,并且没有声明,只要你是用包含汉字的编码格式(例如:utf-8,utf-16,utf-32,GBK,GB2312,UCS-2....)进行保存,现代浏览器都能正常解码,因为它们本身带有编码纠错功能,但不推荐这么做!!!
但是如果你使用不包含汉字的编码格式(例:ASCII,ISO-8859-1...)进行保存,浏览器的纠错功能也回天乏术了。

六、HTML,CSS,JS使用Unicode编码的方式

html
<head> <meta charset="UTF-8"> <style> .css:after { content: "\0041"; } </style> </head> <body> <h1 class="css">CSS- </h1> <!-- HTML十进制: &#; --> <!-- HTML十六进制: &#x; --> <h2>HTML10进制 - &#65;</h2> <h2>HTML16进制 - &#x41;</h2> <h3>JavaScript - <span id="js"></span></h3> <script> document.getElementById('js').innerHTML = '\u0041'; </script> </body>
8793674-a32a8f546bd080a8
这里其实重要的要说我们常用的字体图标

先来认识下常用的font-family属性

8793674-4d237d56e03e75ba

页面效果

8793674-0c6e9523e9edfca1

页面中的font-family属性,浏览器会选择列表中第一个该计算机上有安装的字体,或者是通过 @font-face 指定的可以直接下载的字体。

所以一般我们都会在font-family属性上定义多个字体,以防止计算机匹配不到。

通过工具解析字体文件内容,查看字体自负与Unicode编码的联系。
8793674-49096c7fe8abc73e
然后再通过解析三种字体对比查看。
8793674-11cc04e1cde03aee

font-family => 计算机本地安装的字体文件 => 具体显示的字符样式,这其中的映射关系就很明显了。

我们使用的提提图标一般都会使用@font-face
那么下载字体图标文件,比如阿里妈妈字体图标,我们可以获得如下示例代码。

css
@font-face { font-family: "iconfont"; src: url('iconfont.eot?t=1594826325393'); /* IE9 */ src: url('iconfont.eot?t=1594826325393#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAA.......AA==') format('woff2'), url('iconfont.woff?t=1594826325393') format('woff'), url('iconfont.ttf?t=1594826325393') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ url('iconfont.svg?t=1594826325393#iconfont') format('svg'); /* iOS 4.1- */ } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>IconFont Demo</title> <link rel="stylesheet" href="iconfont.css"> <style> .icon{font-size: 40px !important;} </style> </head> <body> <div> <span class="icon iconfont">&#x41;</span> <span class="icon iconfont">&#x42;</span> <span class="icon iconfont">&#x43;</span> </div> <script> </script> </body> </html>
8793674-5f2e755dd5e3c6c1 8793674-b99171e18cb7f4dc

这是我随便找的三个字体图标,虽然在页面上显示了图形,但控制台的document文档中依然是ABC,这是因为浏览器解析document文档时可不会等你的字体文件加载完,它也不需要,无论是本地的字体文件,还是网络的字体文件。他会以自己的方式根据Unicode编码表显示字符,这个显示的字符都是固定的一种字体的,因为不会按照你的dom的font-family去解析字体,这也是为什么控制台document中的字体永远都是那样的原因。

但是这样就有一个问题,搜索引擎可是会查看这个document文档内容的,让搜索引擎抓取到这些不需要的字符没有必要。所以一般你在阿里妈妈字体库里添加的图标,它们的码点默认都是再Unicode的第一个基础平面私人区域,这块区域码点为E000-F8FF。

在Unicode中,私人使用区(简称:私用区;英语:Private Use Areas,简称:PUA)指其解释未在Unicode标准中指定,而是由合作用户之间的私人协议决定其用途的一系列码位。目前定义了三个私人使用区:一个在基本多语言平面(U+E000-U+F8FF)中,另外两个几乎包含了整个第15和第16平面(分别为U+F0000-U+FFFFD,U+100000-U+10FFFD)。

私人使用区字符的分配,可不由字面意义上的私人决定;一些组织已经发布了一些分配计划。但根据其定义,私人使用区相同的代码点可被分配为不同的字符,因此用户可能因安装了某种字体,看到其显示为一种形态,但使用了其他字体的用户可能看到完全不同的字符。

8793674-05009d20857e1b4a

绿色为私人区域,还有16,17平面也是私人区域,可在Unicode编码(中)查看

我下载的的码点不在这个区域,是因为我要演示效果,把编码手动修改了。
这回我再改回来。。

8793674-ff6fb3076845c2ab
html
<div> <span class="icon iconfont">&#xe69c;</span> <span class="icon iconfont">&#xe69d;</span> <span class="icon iconfont">&#xe69e;</span> </div>

我改回来之后的效果

8793674-0830064eda4cd48b

好了,这样就不会影响到谁了,搜索引擎不会抓取没有意义的字符的。

至此,Unicode编码相关的东西就告一段落,但这不是结束,而仅仅是开始。