前言
以前用到字符串的方法时候,并不会深刻的去思考其中的原理,所以在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) | 前缀 | 示例 |
---|---|---|
二进制 binary | 0b 0B | 0b11 = 2+1=3 |
八进制 octal | 0o 0O 0 | 0o11 = 8+1=9 |
十进制 decimal | 无前缀 | 11 = 11 |
十六进制 hex | 0x 0X | 0x11 |
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
,字母B
是0x42
,以此类推:
字母 | ASCII编码 |
---|---|
A | 0x41 |
B | 0x42 |
C | 0x43 |
D | 0x44 |
具体可以查看 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编码 |
---|---|---|
中 | 0x4e2d | 0xe4b8ad |
文 | 0x6587 | 0xe69687 |
编 | 0x7f16 | 0xe7bc96 |
码 | 0x7801 | 0xe7a081 |
正确识别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" ===> 𠮷
复制代码
上面代码中,汉字“𠮷”(注意,这个字不是”吉祥“的”吉“)的码点是0x20BB7
,UTF-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编码可以把任意长度的二进制数据变为纯文本,且只包含A
Z
、a
z
、0
~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
分别是41
、42
、43
,按6bit分组得到16
、20
、19
和3
:
因为6位整数的范围总是0
63
,所以,能用64个字符表示:字符A
Z
对应索引0
25
,字符a
z
对应索
引26
51
,字符0
9
对应索引52
~61
,最后两个索引62
、63
分别用字符+
和/
表示。
┌───────────────┬───────────────┬───────────────┐
│ 41 │ 42 │ 43 │ // 十六进制 0x
└───────────────┴───────────────┴───────────────┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│0│1│0│0│0│0│0│1│0│1│0│0│0│0│1│0│0│1│0│0│0│0│1│1│ // 二进制 0b
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌───────────┬───────────┬───────────┬───────────┐
│ 16 │ 20 │ 19 │ 3 │ // 10进制
└───────────┴───────────┴───────────┴───────────┘
- 首先取ABC对应的ASCII码值
A : 65 => 0b01000001 、B : 66 => 0b01000010、C : 67 => 0b01000011
- 再取二进制值
A : 01000001、B : 01000010、C : 01000011
- 然后把这三个字节的二进制码接起来
010000010100001001000011
- 再以6位为单位分成4个数据块并在最高位填充两个0后形成4个字节的编码后的值
00010000、00010100、00001001、00000011
- 再把这4个字节数据转化成10进制数
16、20、19、3
- 最后根据Base64给出的64个基本字符表,查出对应的ASCII码字符
Q、U、J、D
这里的值实际就是数据在字符表中的索引。
解码过程就是把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.-_
以外,都进行%
替换:
- 如果字符是
A
Z
,a
z
,0
~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-Z | 69个 | ES3 过时产物,使用 encodeURI() 和 encodeURIComponent() 替代它 |
encodeURI/decodeURI | !#$'()*+,/:;=?@-._~0-9a-zA-Z | 82个 | 对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的:;/?:@&=+$,# ,因此它很适用于来编码完整的url,因为这些字符是用来分割主机和路径的。它对应的解码是decodeURI |
encodeURIComponent/decodeURIComponent | !'()*-._~0-9a-zA-Z | 71个 | 它跟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)编号;第三部分是一个分号。
比如,要显示小于号(<),就可以写 <
或者 <
。
用实体(Entity)
名字的好处是比较好理解,一看lt,大概就猜出是less than的意思,但是其劣势在于并不是所有的浏览器都支持最新的Entity名字。而实体(Entity)编号,各种浏览器都能处理。
最常用的字符实体
显示结果 | 描述 | 实体名称 | 实体编号 |
---|---|---|---|
空格 | |   | |
< | 小于号 | < | < |
大于号 | > | > | |
& | 和号 | & | & |
" | 引号 | " | " |
' | 撇号 | ' (IE不支持) | ' |
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));// <p><b> 123&456 </b></p>
复制代码
// 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 = "<p><b> 123&456 </b></p>";
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,将字符串转为<script>alert(2);</script>
;此时,浏览器将能正确解析,因为浏览器接收到实体字符后,转成对应的尖括号等。
b. 不使用HTMLEncode,浏览器一看到<
,便认为是html标签的开始,直接把刚才的字符串当脚本执行了,这就是xss漏洞。
2,HTMLDecode将字符实体转成< > & " '
使用场景: 后端将已经转义后的内容显示到页面;比如<script>alert(2);</script>
;
js收到后:
a. 前端进行HTMLDecode,则可以直接dom操作,将标签显示到页面。
b. 前端没有HTMLDecode,则原样输出<script>alert(2);</script>
,但此时并没有执行。
如何防止XSS攻击
优雅解决方案
将用户输入的进行编码,将< 、"、&、> 等特殊字符进行转义,让浏览器当成字符串显示。
// artTemplete 中进行XSS 防御转义方式。
var escapeMap = {
"<": "<",
">": ">",
'"': """,
"'": "'",
"&": "&"
};
var escapeFn = function (s) {
return escapeMap[s];
};
var escapeHTML = function (content) {
return toString(content)
.replace(/&(?![\w#]+;)|[<>"']/g, escapeFn);
};
复制代码
// 抽离成可配置的匹配列表
const matchList = {
'<': '<',
'>': '>',
'&': '&',
'"': '"',
'"': '"',
''': "'"
}
// 字符过滤器
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;
}
复制代码
插件
- 前端防止XSS攻击的插件 js-xss库
小结
- Base64编码和URL编码都是编码算法,它们不是加密算法;
- Base64编码的目的是把任意二进制数据编码为文本,但编码后数据量会增加1/3。
- URL编码的目的是把任意文本数据编码为%前缀表示的文本,便于浏览器和服务器处理;
- HTML 转义字符目的有些字符在ASCII字符集没有定义,需要使用转义字符串来表示;同时特殊HTML字符如
<
>
等不能直接当作文本中的符号来使用。