衆所周知,任何數據在計算機中都是以二進制的方式存儲的。全世界的數據囊括起來有:英文字母、英文標點、阿拉伯數字、文字、符號。那麼在計算機內部是如何表示這些數據的呢?最初的ASCII碼對英文字母、英文標點、阿拉伯數字進行編碼,一個字節表示一個字符,只用了低7位,一共2**7=128個字符,學習到這裏的時候,我特地數了下咱們的鍵盤上,有52個字母(分大小寫),42個英文標點,再加上1個空格符,總共是95個可顯示的字符,那剩下的33個字符是什麼字符呢,能夠查看wiki,對咱們理解計算機編碼沒什麼用,因此這裏先忽略。html
很顯然,這樣的字符集,是沒法處理咱們廣袤的語言文字的。因此出現了統一編碼字符集Unicode,它是全世界範圍內的統一編碼規則,惟一的編碼對應惟一的符號,在ASCII碼的基礎上,加入了對各類語言文字甚至新型的表情等符號的編碼,而且仍然在不斷的增修中。在表示一個Unicode的字符時,一般會用「U+」而後緊接着一組十六進制的數字來表示這一個字符。Unicode的編碼空間是U+0000至U+10FFFF,在這個空間內,分爲17(0-16)組空間,每組被稱爲平面,第0組平面,又稱爲基本多文種平面(BMP),範圍在U+0000至U+FFFF,其餘平面看下圖瞭解一下。另附上字符對應表unicode.org和漢字對應表,不妨也打開看看。算法
若是有心,上面的連接你已經打開了,你會看到大多字符都是使用U+xxxx這樣的2個字節16bits表示的,例如字「回」,它的Unicode碼是U+56DE
。每一個字符的編碼有了,那在計算機中怎麼存儲和處理一連串的字符呢,也就是說編碼規則是如何實現的?有UTF-8/UTF-16/UTF-32三種實現方式,其中經常使用的是UTF-8和UTF-16。api
在查閱unicode的時候,我老是會看到UCS-2 UCS-4這樣的描述。「UCS-2 is outdated, though still widely used in software」,Unicode English wiki上有這麼一句話。也就是說USC-2是一種過期的叫法,它還有一個最新的叫法UTF-16,這樣是否是就明白了?由於2是指2個字節,16是指16位。固然UTF-16和UCS-2確實不徹底相等,可是沒有必要再深究了。下面一段話摘自wiki:bash
Unicode defines two mapping methods: the Unicode Transformation Format (UTF) encodings, and the Universal Coded Character Set (UCS) encodings. An encoding maps (possibly a subset of) the range of Unicode code points to sequences of values in some fixed-size range, termed code values. All UTF encodings map all code points (except surrogates) to a unique sequence of bytes.[54] The numbers in the names of the encodings indicate the number of bits per code value (for UTF encodings) or the number of bytes per code value (for UCS encodings). UTF-8 and UTF-16 are probably the most commonly used encodings. UCS-2 is an obsolete subset of UTF-16; UCS-4 and UTF-32 are functionally equivalent.app
UTF-8是一種變長的編碼方式,可使用1~6個字節表示一個字符,可是Unicode最大隻到U+10FFFF,因此最多4個字節。它的編碼規則以下:編輯器
字符的Unicode編碼範圍 | UTF-8 編碼方式
(十六進制) | (二進制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
複製代碼
如此編碼完成,試試看「回」字的UTF-8編碼結果,答案應該是11100101 10011011 10011110
,十六進制表示是e59b9e
,最後咱們能夠用編輯器來驗證下: ide
UTF-16使用固定的一個或兩個無符號的16位整數來編碼。它的編碼規則以下:學習
U
,若是小於0x10000,編碼結果就是它本身U
在0x10000-0x10FFFF範圍內,U' = U - 0x10000
,且U'
確定不超過20位的,則將它分紅兩個10位,分別填充入W1 = 0xD800
的後10位和W2 = 0xDC00
的後10位中 看起來像是這樣:U' = yyyyyyyyyyxxxxxxxxxx W1 = 110110yyyyyyyyyy W2 = 110111xxxxxxxxxx 複製代碼
頗有意思哈~,試試計算U+10437
的UTF-16的編碼結果,答案是1101 1000 0000 0001 1101 1100 0011 0111
,十六進制的表示結果是d801 dc37
。這裏還有一個大端序和小端序的概念,這是描述CPU如何向內存寫數據的概念,計算機在處理2個8位字節的時候,若將高位字節存放在低內存地址,則稱爲「大端序」。若將高位字節存放在高位地址,則稱爲「小端序」。 那麼d801 dc37
則是大端序Big endian,01d8 37dc
則是小端序little endian。一樣能夠在編輯器中去驗證UTF-16的編碼結果。ui
js內部使用的編碼是UTF-16,咱們不妨來看下編碼相關的api。this
String.prototype.charCodeAt
此方法返回的是字符對應的Unicode碼的整數值。例如:
var sentence = '回家吧!';
var index = 0;
console.log('The character code ' + sentence.charCodeAt(index) + ' is equal to ' + sentence.charAt(index));
// expected output: "The character code 22238 is equal to 回"
複製代碼
那麼變體,看下「回」的16進製表示:
var sentence = '回家吧!';
var index = 0;
console.log('The character code ' + sentence.charCodeAt(index).toString(16) + ' is equal to ' + sentence.charAt(index));
// expected output: "The character code 56de is equal to 回"
複製代碼
String.fromCharCode
此方法將UTF-16轉換爲字符串。
console.log(String.fromCharCode(22238));
// expected output: "回"
複製代碼
Base64是以每3個8位爲一個單元,轉換爲4個6位的格式,6位的高兩位填充0,這樣的8位一共有2**6=64個字符,對應有一個Base64的索引表,找出索引表對應的可打印字符,如此便生成一個Base64字符。但有可能原數據不是3的整數倍,那麼若是餘下兩個輸入數據,在編碼結果後加1個「=」;若是餘下一個輸入數據,編碼結果後加2個「=」。在這個Base64的算法中,要清晰的認識一點,當string的編碼方式不一樣時,獲得的Base64 string結果也會不一樣。
在Javascript中,有兩個內置的方法btoa()和atob()分別對ASCII碼進行Base64的編碼和解碼。可是此方法只支持ASCII碼,Unicode string怎麼辦?MDN給的解決方案是:
// ucs-2 string to base64 encoded ascii
function utoa(str) {
return window.btoa(unescape(encodeURIComponent(str)));
}
// base64 encoded ascii to ucs-2 string
function atou(str) {
return decodeURIComponent(escape(window.atob(str)));
}
// Usage:
utoa('✓ à la mode'); // 4pyTIMOgIGxhIG1vZGU=
atou('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"
utoa('I \u2661 Unicode!'); // SSDimaEgVW5pY29kZSE=
atou('SSDimaEgVW5pY29kZSE='); // "I ♡ Unicode!"
複製代碼
看到這裏的時候,有人會有疑問,在解碼的時候,若是是客戶端或者服務端,難道也能夠正確的解碼嗎?我認爲答案是能夠的。首先看下另外一套網上的解決方案:
var Base64 = {
// 轉碼錶
table : [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O' ,'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/'
],
UTF16ToUTF8 : function(str) {
var res = [], len = str.length;
for (var i = 0; i < len; i++) {
var code = str.charCodeAt(i);
if (code > 0x0000 && code <= 0x007F) {
// 單字節,這裏並不考慮0x0000,由於它是空字節
// U+00000000 – U+0000007F 0xxxxxxx
res.push(str.charAt(i));
} else if (code >= 0x0080 && code <= 0x07FF) {
// 雙字節
// U+00000080 – U+000007FF 110xxxxx 10xxxxxx
// 110xxxxx
var byte1 = 0xC0 | ((code >> 6) & 0x1F);
// 10xxxxxx
var byte2 = 0x80 | (code & 0x3F);
res.push(
String.fromCharCode(byte1),
String.fromCharCode(byte2)
);
} else if (code >= 0x0800 && code <= 0xFFFF) {
// 三字節
// U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
// 1110xxxx
var byte1 = 0xE0 | ((code >> 12) & 0x0F);
// 10xxxxxx
var byte2 = 0x80 | ((code >> 6) & 0x3F);
// 10xxxxxx
var byte3 = 0x80 | (code & 0x3F);
res.push(
String.fromCharCode(byte1),
String.fromCharCode(byte2),
String.fromCharCode(byte3)
);
} else if (code >= 0x00010000 && code <= 0x001FFFFF) {
// 四字節
// U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
} else if (code >= 0x00200000 && code <= 0x03FFFFFF) {
// 五字節
// U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
} else /** if (code >= 0x04000000 && code <= 0x7FFFFFFF)*/ {
// 六字節
// U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
}
}
return res.join('');
},
UTF8ToUTF16 : function(str) {
var res = [], len = str.length;
var i = 0;
for (var i = 0; i < len; i++) {
var code = str.charCodeAt(i);
// 對第一個字節進行判斷
if (((code >> 7) & 0xFF) == 0x0) {
// 單字節
// 0xxxxxxx
res.push(str.charAt(i));
} else if (((code >> 5) & 0xFF) == 0x6) {
// 雙字節
// 110xxxxx 10xxxxxx
var code2 = str.charCodeAt(++i);
var byte1 = (code & 0x1F) << 6;
var byte2 = code2 & 0x3F;
var utf16 = byte1 | byte2;
res.push(String.fromCharCode(utf16));
} else if (((code >> 4) & 0xFF) == 0xE) {
// 三字節
// 1110xxxx 10xxxxxx 10xxxxxx
var code2 = str.charCodeAt(++i);
var code3 = str.charCodeAt(++i);
var byte1 = (code << 4) | ((code2 >> 2) & 0x0F);
var byte2 = ((code2 & 0x03) << 6) | (code3 & 0x3F);
var utf16 = ((byte1 & 0x00FF) << 8) | byte2
res.push(String.fromCharCode(utf16));
} else if (((code >> 3) & 0xFF) == 0x1E) {
// 四字節
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
} else if (((code >> 2) & 0xFF) == 0x3E) {
// 五字節
// 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
} else /** if (((code >> 1) & 0xFF) == 0x7E)*/ {
// 六字節
// 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
}
}
return res.join('');
},
encode : function(str) {
if (!str) {
return '';
}
var utf8 = this.UTF16ToUTF8(str); // 轉成UTF8
var i = 0; // 遍歷索引
var len = utf8.length;
var res = [];
while (i < len) {
var c1 = utf8.charCodeAt(i++) & 0xFF;
res.push(this.table[c1 >> 2]);
// 須要補2個=
if (i == len) {
res.push(this.table[(c1 & 0x3) << 4]);
res.push('==');
break;
}
var c2 = utf8.charCodeAt(i++);
// 須要補1個=
if (i == len) {
res.push(this.table[((c1 & 0x3) << 4) | ((c2 >> 4) & 0x0F)]);
res.push(this.table[(c2 & 0x0F) << 2]);
res.push('=');
break;
}
var c3 = utf8.charCodeAt(i++);
res.push(this.table[((c1 & 0x3) << 4) | ((c2 >> 4) & 0x0F)]);
res.push(this.table[((c2 & 0x0F) << 2) | ((c3 & 0xC0) >> 6)]);
res.push(this.table[c3 & 0x3F]);
}
return res.join('');
},
decode : function(str) {
if (!str) {
return '';
}
var len = str.length;
var i = 0;
var res = [];
while (i < len) {
code1 = this.table.indexOf(str.charAt(i++));
code2 = this.table.indexOf(str.charAt(i++));
code3 = this.table.indexOf(str.charAt(i++));
code4 = this.table.indexOf(str.charAt(i++));
c1 = (code1 << 2) | (code2 >> 4);
res.push(String.fromCharCode(c1));
if (code3 != -1) {
c2 = ((code2 & 0xF) << 4) | (code3 >> 2);
res.push(String.fromCharCode(c2));
}
if (code4 != -1) {
c3 = ((code3 & 0x3) << 6) | code4;
res.push(String.fromCharCode(c3));
}
}
return this.UTF8ToUTF16(res.join(''));
}
};
複製代碼
兩套方案對比,輸出的Base64 string是同樣的。在第二套方案中,先將UTF16編碼的string處理爲utf8編碼的字符串,再將utf8 string轉換爲Base64。兩個方案獲得的結果是同樣的,由於第一套方案中,encodeURIComponent也是作的utf8編碼處理。因此我認爲只要是加解密雙方使用統一編碼方式,獲得的信息確定是同樣的,至於他們如何解碼,自是他們的事情。
最後附一個公式,由於我本身總是會忘記,在寫這篇文章的過程當中還不停翻本身寫在筆記本上的:
1字節(byte) = 8位(bit)
1字符 = 一個或多個字節
developer.mozilla.org/en-US/docs/…