前端编码那些事

1,003 阅读15分钟

前言

以前用到字符串的方法时候,并不会深刻的去思考其中的原理,所以在ES6新增的这些方法里就有点蒙圈了,于是想要搞清楚为什么会新增这些方法,以及如何使用这些方法。在这之前需要先来回顾下字节进制这两个概念。

字节

字节(英语:Byte),通常用作信息计量单位,不分数据类型。是通信和数据存储的概念。

一个字节能存储8位2进制数据(这个是规范,需要刻在DNA里面)

1Byte = 8 bit 

2^8是256,1个字节能表示的数就是0~255,共256种可能性。

1位16进制数能表示为4位2进制,所以一个字节能表示2个16进制。

1KB = 1024 B 2^10 Byte 
1MB = 1024 KB 2^20 Byte 
1GB = 1024 MB 2^30 Byte

其实是200Mb/s,但是文件是以Byte为单位的,而不是bit,所以需要换算一下.

200Mb / 8 = 25 MB

进制

进制基数(radix)前缀示例
二进制 binary0b 0B0b11 = 2+1=3
八进制 octal0o 0O 00o11 = 8+1=9
十进制 decimal无前缀11 = 11
十六进制 hex0x 0X0x11
0b10 // 二进制

0o10 // 八进制

0xff // 16进制

进制转换

  • parseInt(str,radix) : 将字符串str按照radix进制编码方式转换为十进制返回
  • Number.toString(radix) : 返回表示该数字的指定进制形式的字符串,radix 支持[2,36]
parseInt("8",10) // ==> 8 (十进制)
parseInt("13",8) // ==> 11 (十进制)

(10).toString(2) // ==> 1010 (二进制)
(18).toString(16) // ==> 12 (十六进制)

// 任意进制转换
function tansformRadix(num,m,n){
  var s = num+'';
  var result = parseInt(s,m).toString(n);
  return result;
}

javascript字符表示

JavaScript共有6种方法可以表示一个字符

'\z' === 'z'  // true 
'\172' === 'z' // true 8进制
'\x7A' === 'z' // true 16进制
'\u007A' === 'z' // true unicode编码集
'\u{7A}' === 'z' // true es6

字符编码

ASCII码

ASCII码就是一种编码,字母A的编码是十六进制的0x41,字母B0x42,以此类推:

字母ASCII编码
A0x41
B0x42
C0x43
D0x44

具体可以查看 ASCII 编码对照表

"A".charCodeAt(0) // ==> 65
"A".codePointAt(0) // ==> 65

String.fromCharCode("0x41") // ==> A
String.fromCharCode(65) // ==> A

String.fromCodePoint(65) // ==> A
String.fromCodePoint("0x41") ===> A

Unicode 编码

由于ASCII编码最多只能有127个字符,要想对更多的文字进行编码,就需要用Unicode。而中文的使用Unicode编码就是0x4e2d,使用UTF-8则需要3个字节编码:

汉字Unicode编码UTF-8编码
0x4e2d0xe4b8ad
0x65870xe69687
0x7f160xe7bc96
0x78010xe7a081

正确识别Unicode编码

JavaScript允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的Unicode码点。unicode字符集的范围是U+0000到U+10ffff, 这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。(比如\u20BB7,JavaScript会理解成\u20BB+7)。

console.log('\u4e2d\u6587\u7f16\u7801') // 中文编码

console.log("\uD842\uDFB7") //  𠮷
console.log("\u20BB7") //  ₻7 出现乱码

// **ES6**对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。
console.log("\u{20BB7}") //  𠮷

js的字符在内存中是按UTF-16对字符进行编码,也就是说js默认操作字符的基本单位,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),JavaScript会认为它们是两个字符,这就会出现问题。

方法描述
fromCharCode受一个指定的 Unicode 值,然后返回一个字符串。值是 0 - 65535 之间的整数,即对不大于0xFFFF的码点才有效
charCodeAt返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数,即对不大于0xFFFF的码点才有效
fromCodePoint使用指定的 Unicode 编码位置创建的字符串
codePointAt返回值是在字符串中的给定索引的编码单元体现的数字

charAt vs charCodeAt vs codePointAt

var s = "𠮷";
s.length // 2
s.charAt(0) // ''
s.charAt(1) // ''

s.charCodeAt(0) // 55362 ==> (55362).toString(16) ==> D842
s.charCodeAt(1) // 57271 ==> (57271).toString(16) ==> DFB7

// 超出0xffff这个范围的字符,必须用两个双字节的形式表示。
console.log("\uD842\uDFB7") // 𠮷

// ES6 
"𠮷".codePointAt(0) // 134071 ==> (134071).toString(16) ==> 20bb7

String.fromCharCode(134071) // 乱码
String.fromCodePoint(134071)  // 𠮷
"\u20bb7" ===> 𠮷

上面代码中,汉字“𠮷”(注意,这个字不是”吉祥“的”吉“)的码点是0x20BB7UTF-16编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript是不能正确处理,字符串长度会误判为2,而且charAt方法无法读取整个字符,charCodeAt方法只能分别返回前两个字节和后两个字节的值。

ES6提供了codePointAt方法,能够正确处理4个字节储存的字符,返回一个字符的码点

for...of

var text = String.fromCodePoint(0x20BB7);

for(let i = 0; i < text.length; i++){
   console.log(text[i]);
}
// �
// �

for(let i of text){
  console.log(i);
}
// "𠮷"
// String.fromCodePoint(0x20BB7).codePointAt(0).toString(16) // ==> 20BB7

除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

String.formCharCode String.charAt() String.charCodeAt() String.formCodePoint()

UTF-8 vs UTF-16

UTF-16 就是任何字符对应的数字都用两个字节来保存.

UTF-8 表示一个字符是可变的,可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,当字符在ASCII码的范围时,就用一个字节表示,保留了ASCII字符一个字节的编码做为它的一部分。处理程序一个字节一个字节的来读取,然后再根据字节中开头的bit标志来识别是该把1个还是两个或三个字节做为一个单元来处理.你用UTF-8来表示时必须遵守这样的约定的规则:

  • 0xxxxxxx,如果是这样的01串,也就是以0开头后面是啥就不用管了XX代表任意bit。就表示把一个字节做为一个单元。就跟ASCII完全一样。
  • 110xxxxx 10xxxxxx.如果是这样的格式,则把两个字节当一个单元
  • 1110xxxx 10xxxxxx 10xxxxxx 如果是这种格式则是三个字节当一个单元.

要知道文件具体是哪种编码方式,需要判断文本开头的标志,下面是所有编码对应的开头标志

开头标志编码方式
EF BB BF    UTF-8
FE FF     UTF-16/UCS-2, little endian
FF FE     UTF-16/UCS-2, big endian
FF FE 00 00  UTF-32/UCS-4, little endian
00 00 FE FF  UTF-32/UCS-4, big-endian
// 应用场景:encodeURI 对这个url进行编码
encodeURI("http://www.cnblogs.com/season-huang/some other thing");
 ==> http://www.cnblogs.com/season-huang/some%20other%20thing" 空格中文等特殊字符被转义

// 应用场景:当你需要编码URL中的参数的时候,那么encodeURIComponent是最好方法
encodeURIComponent("http://www.baidu.com?callback=xxx") // ==> http%3A%2F%2Fwww.baidu.com%3Fcallback%3Dxxx 对url参数或hash 等部分进行单独编码

Base64编码

Base64编码可以把任意长度的二进制数据变为纯文本,且只包含AZaz0~9+/=这些字符。它的原理是把3字节的二进制数据按6bit一组,用4个int整数表示,然后查表,把int整数用索引对应到字符,得到编码后的字符串。例如,电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用Base64编码,然后以文本的形式传送。

Base64编码的思想是:采用64个基本的ASCII码字符对数据进行重新编码。

  • 1、将需要编码的数据拆分成字节数组,以3个字节为一组,按顺序排列24位数据,再把这24位数据分成4组,即每组6位;
  • 2、再在每组的的最高位前补两个0凑足一个字节,这样就把一个3字节为一组的数据重新编码成了4个字节;
  • 3、当所要编码的数据的字节数不是3的整倍数,也就是说在分组时最后一组不够3个字节,这时在最后一组填充1到2个0字节,并在最后编码完成后在结尾添加1到2个=号。

base64 编码解码过程

举个例子:3个byte数据ABC分别是414243,按6bit分组得到1620193

因为6位整数的范围总是063,所以,能用64个字符表示:字符AZ对应索引025,字符az对应索 引2651,字符09对应索引52~61,最后两个索引6263分别用字符+/表示。

┌───────────────┬───────────────┬───────────────┐
│      414243// 十六进制 0x
└───────────────┴───────────────┴───────────────┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│010000010100001001000011// 二进制 0b
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌───────────┬───────────┬───────────┬───────────┐
│    1620193// 10进制
└───────────┴───────────┴───────────┴───────────┘


- 首先取ABC对应的ASCII码值
A : 65 => 0b01000001B : 66 => 0b01000010C : 67 => 0b01000011
- 再取二进制值
A : 01000001B : 01000010C : 01000011
- 然后把这三个字节的二进制码接起来
010000010100001001000011
- 再以6位为单位分成4个数据块并在最高位填充两个0后形成4个字节的编码后的值
00010000000101000000100100000011
- 再把这4个字节数据转化成10进制数
1620193
- 最后根据Base64给出的64个基本字符表,查出对应的ASCII码字符
QUJD
这里的值实际就是数据在字符表中的索引。
解码过程就是把4个字节再还原成3个字节再根据不同的数据形式把字节数组重新整理成数据。
  • Base64编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。

  • 和URL编码一样,Base64编码是一种编码算法,不是加密算法。

  • 如果把Base64的64个字符编码表换成32个、48个或者58个,就可以使用Base32编码,Base48编码和Base58编码。字符越少,编码的效率就会越低。

字符串转base64 & base64转字符串

//字符串转base64

function encode(str){
    // 对字符串进行编码
    var encode = encodeURI(str);

    // 对编码的字符串转化base64
    var base64 = btoa(encode);
    return base64;

}

 

// base64转字符串
function decode(base64){
    // 对base64转编码
    var decode = atob(base64);
    // 编码转字符串
    var str = decodeURI(decode);
    return str;
}

图片转base64 & base64转图片

// data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAAAxxxxxxxxYII=
function image2Base64(img) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, img.width, img.height);
    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
}


function getImgBase64(){
    var base64="";
    var img = new Image();
    img.src="img/test.jpg";
    img.onload = function(){
        base64 = image2Base64(img);
        alert(base64);
    }
}

URL编码

为什么需要对URI进行编码?

对于Url来说,之所以要进行编码,是因为Url中有些字符会引起歧义。因为出于兼容性考虑,很多服务器只识别ASCII字符。但如果URL中包含中文、日文这些非ASCII字符怎么办?

一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。这是因为网络标准RFC 1738做了硬性规定:

原文:"...Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL."
翻译:“只有字母和数字[0-9a-zA-Z]、一些特殊符号“$-_.+!*'(),”[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。”

该网络标准却没有规定说怎么进行编码,交给了浏览器来自己来控制,浏览器目前的一个通用的URL编码规则是除了a-zA-Z0-9.-_以外,都进行%替换:

  • 如果字符是AZaz0~9以及-_.*,则保持不变;
  • 如果是其他字符,先转换为UTF-8编码,然后对每个字节以%XX表示。
 https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87

js 如何对URL进行编码?

Javascript中提供了3对函数用来对Url编码以得到合法的Url

方法安全字符范围个数使用场景
escape/unescape*/@+-._0-9a-zA-Z69个ES3 过时产物,使用 encodeURI() 和 encodeURIComponent() 替代它
encodeURI/decodeURI!#$'()*+,/:;=?@-._~0-9a-zA-Z82个对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的:;/?:@&=+$,#,因此它很适用于来编码完整的url,因为这些字符是用来分割主机和路径的。它对应的解码是decodeURI
encodeURIComponent/decodeURIComponent!'()*-._~0-9a-zA-Z71个它跟encodeURI的区别就是,encodeURI是对整个url进行编码,而encodeURIComponent是对url的个别部分进行编码。因此,像;/?:@&=+$,# 这些也是会被编码

HTML 编码

为了正确显示 HTML 页面,Web 浏览器必须了解页面中使用的字符集。

<meta charset="UTF-8">

HTML 字符 & 转义字符

转义对照表

HTML 字符实体(Character Entities)& 转义字符串(Escape Sequence)

在HTML中,定义转义字符串的原因有两个:第一个原因是像<>这类符号已经用来表示HTML标签,因此就不能直接当作文本中的符号来使用。为了在HTML文档中使用这些符号,就需要定义它的转义字符串。当解释程序遇到这类字符串时就把它解释为真实的字符。在输入转义字符串时,要严格遵守字母大小写的规则。第二个原因是,有些字符在ASCII字符集中没有定义,因此需要使用转义字符串来表示。

转义字符串(Escape Sequence),即字符实体(Character Entity)分成三部分:第一部分是一个&符号,英文叫ampersand;第二部分是实体(Entity)名字或者是#加上实体(Entity)编号;第三部分是一个分号。

比如,要显示小于号(<),就可以写 &lt; 或者 &#60;

实体(Entity)名字的好处是比较好理解,一看lt,大概就猜出是less than的意思,但是其劣势在于并不是所有的浏览器都支持最新的Entity名字。而实体(Entity)编号,各种浏览器都能处理。

最常用的字符实体

显示结果描述实体名称实体编号
 空格&nbsp;&#160;
<小于号&lt;&#60;
大于号&gt;&#62;
&和号&amp;&#38;
"引号&quot;&#34;
'撇号&apos; (IE不支持)&#39;

HTML字符转义与反转义

// HTML转义

function HTMLEncode(html) {
    let temp = document.createElement("div"); // div 也可替换pre
    (temp.textContent != null) ? (temp.textContent = html) : (temp.innerText = html);
    const output = temp.innerHTML;
    temp = null;

    return output;
}



const tagText = "<p><b> 123&456 </b></p>";

console.log(HTMLEncode(tagText));// &lt;p&gt;&lt;b&gt; 123&amp;456 &lt;/b&gt;&lt;/p&gt;
// HTML反转义

function HTMLDecode(text) {
    if (text === null || text === undefined || text === '') {
        return ''
    }
    if (typeof text !== 'string') {
        return String(text)
    }

    let temp = document.createElement("div"); // div 也可替换pre
    temp.innerHTML = text;
    const output = temp.textContent || temp.innerText;
    temp = null;
    
    return output;

}

const tagText = "&lt;p&gt;&lt;b&gt; 123&amp;456 &lt;/b&gt;&lt;/p&gt;";
console.log(HTMLDecode(tagText)); //<p><b> 123&456 </b></p>

使用场景

当用户在input输入框输入HTML标签时,为了防止XSS攻击,需要进行转义。显示时则需要进行反转义

1,HTMLEncode将< > & " '转成字符实体 使用场景: 

  • (1)用户在页面中录入(比如输入框)<script>alert(2);</script>, js将该内容提交给后端保存 
  • (2)显示时,后端将字符串返回前端;js接收到之后: 

a. 使用HTMLEncode,将字符串转为&lt;script&gt;alert(2);&lt;/script&gt;此时,浏览器将能正确解析,因为浏览器接收到实体字符后,转成对应的尖括号等。  b. 不使用HTMLEncode,浏览器一看到<,便认为是html标签的开始,直接把刚才的字符串当脚本执行了,这就是xss漏洞。 

2,HTMLDecode将字符实体转成< > & " '  使用场景: 后端将已经转义后的内容显示到页面;比如&lt;script&gt;alert(2);&lt;/script&gt;  js收到后:  a. 前端进行HTMLDecode,则可以直接dom操作,将标签显示到页面。  b. 前端没有HTMLDecode,则原样输出<script>alert(2);</script>,但此时并没有执行。

如何防止XSS攻击

优雅解决方案

将用户输入的进行编码,将< 、"、&、> 等特殊字符进行转义,让浏览器当成字符串显示。

 // artTemplete 中进行XSS 防御转义方式。
 var escapeMap = {
        "<": "&#60;",
        ">": "&#62;",
        '"': "&#34;",
        "'": "&#39;",
        "&": "&#38;"
    };
  var escapeFn = function (s) {
        return escapeMap[s];
    };
    
  var escapeHTML = function (content) {
    return toString(content)
    .replace(/&(?![\w#]+;)|[<>"']/g, escapeFn);
};
// 抽离成可配置的匹配列表
const matchList = {
  '&lt;': '<',
  '&gt;': '>',
  '&amp;': '&',
  '&#34;': '"',
  '&quot;': '"',
  '&#39;': "'"
}

// 字符过滤器 
const HtmlFilter = (text) => { 
    let regStr = '(' + Object.keys(matchList).toString() + ')' // ↑ ------------【*提取匹配列表key值*】.【组数转字符串】 
    regStr = regStr.replace(/,/g, ')|(') // ↑ 通过匹配将其更新为正则的字符串类型 
    const regExp = new RegExp(regStr, 'g') // ↑ ------- 字符串 转 正则 方法 
    return text.replace(regExp, match => matchList[match]) // ↑ ------ 替换方法 (正则, 当前key => 返回当前被匹配的key值) 
} 
export default HtmlFilter
暴力解决方案

通过innerText/textContent 的只能设置文本的特殊性。

function(value){
    if(typeof value !== 'string'){
        return value;
    }
    var str = value || '',
        temp = document.createElement ("div"),
        obj;
    (temp.textContent != undefined ) ? (temp.textContent = str) : (temp.innerText = str);
    obj = temp.innerHTML;
    temp = null;
    return obj;
}
插件

小结

  • Base64编码和URL编码都是编码算法,它们不是加密算法;
  • Base64编码的目的是把任意二进制数据编码为文本,但编码后数据量会增加1/3。
  • URL编码的目的是把任意文本数据编码为%前缀表示的文本,便于浏览器和服务器处理;
  • HTML 转义字符目的有些字符在ASCII字符集没有定义,需要使用转义字符串来表示;同时特殊HTML字符如< > 等不能直接当作文本中的符号来使用。