字符编码 unicode 及其在javascript 中的使用

一、javascript 使用 unicode16 字符集,可以使用中文变量名和函数名

计算机使用 8 位(bit)二进制表示一个字节(Byte),计算机内存最小寻址单位就是 1 字节。
早期为了在计算机上使用同一的方式使用字符,使用无符号整数来标记字符。
ANSI(美国国家标准局)制订了ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),使用一个字节大小的二进制数来编码每个字符。
ASCII已经被国际标准化组织(ISO)定为国际标准,称为ISO 646标准。
一个字节为8位二进制,2的8次方为256,因此有256个字符可以用一个字节来表示(0~255),
但ASCII字符集只设计了128个字符(字母、数字、一些标点符号和控制字符),因此实际上只用到7位二进制,第八位设置为0,剩下的128个编码位置是闲置的。
有的计算机厂商可能会利用闲置的128个空位来制订一些字符的编码,称为OEM字符集。
例如,IBM使用多出来的128位扩展了一个ASCII 扩展表,包含了一些控制符和制表符等等,被广泛使用在电子元件的数据通讯和存储中,但OEM字符集不是通用的标准。

为了编码更多的字符,2个研究字符编码的机构合并研究成果,制订了 unicode 字符集。
unicode 字符集使用使用多个字节来为字符编码,按使用的字节数不同制订了不同方案,所有 unicode 编码方案前 1 个字节(256个码位)的编码对应的字符都是 ASCII 字符集中的字符。
目前 unicode 编码已经达到 64 位,使用 8 个字节标记一个字符。
如果每个字符用2个字节(16位二进制数)来标记,可以编码 65536 个字符(2 的16次方),这基本上已经可以标记世界上所有国家的语言符号,因此,在实际中通用的是UCS-2通用字符集(Universal Character Set,UCS),由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义,UCS 为第一字节的128个空位增补了一个字符集,称为 C1控制符及拉丁文补充-1 (C1 Control and Latin 1 Supplement)。

UCS-2字符集编码法有17个位面,每个位面都用2个字节来标记字符,17个位面可以映射 1,112,064个字符,其中最常用最重要的是编号为 0 的位面,里面包含了最常用的字符编码,称为基本多国语言平面BMP(Basic Multilingual Plane)。
unicode 第 0 平面(BMP)中的编码被划分为不同区段,各国文字符号、控制符、制表符、图形字符等 都有连续的分布,
其中中文简繁体区段是 4E00-9FBF

4E00-9FBF:CJK 统一表意符号 (CJK Unified Ideographs)
0000-007F:C0控制符及基本拉丁文 (C0 Control and Basic Latin)  
0080-00FF:C1控制符及拉丁文补充-1 (C1 Control and Latin 1 Supplement)  
0100-017F:拉丁文扩展-A (Latin Extended-A)  
0180-024F:拉丁文扩展-B (Latin Extended-B)  
0250-02AF:国际音标扩展 (IPA Extensions)  
02B0-02FF:空白修饰字母 (Spacing Modifiers)  
0300-036F:结合用读音符号 (Combining Diacritics Marks)
(......还有很多国家语言和甲骨文等已经不用或极少使用的语言或字符的专用区段)

从第1位面开始,字符的unicode编码已经超出16位二进制数的范围,因此UCS-2无法使用2个字节直接编码BMP位面之外的字符。
但是,在 UCS-2 编码中,区段 UD800 到 UDFFF 的码位是闲置的保留位,
因此,可以使用这个区段中的码位通过一定的转换方式映射到其他位面的 unicode 编码。

在实际的字符传输和存储行为中,为了节省字节数,可能不会直接传输 unicode 编码,而是使用 Unicode转换格式(Unicode Transformation Format,简称为UTF),目前常见的 UTF格式有UTF-7, UTF-7.5, UTF-8, UTF-16, 以及 UTF-32,他们是由 ITTF(Internet Engineering Task Force,互联网工程任务组)组织进行标准化的,UTF-8 和 UTF-16 编码使用比较广泛。

UTF-16 编码:该编码法在 UCS-2 第0位面字符集的基础上利用 D800-DFFF 区段的码位通过一定转换方式将超出2字节的字符编码为一对16比特长的码元(即32bit,4Bytes),称作代理码对 (surrogate pair)。

例如字符'𪚥',他处于编号'2'的位面(总共17个位面,位面编号为16进制数0-10,第0位面可以舍去编号0直接用4位16进制数编码),码位是A6A5,即unicode编码为 2A6A5,它在UTF-16中的代理码对为 d869 dea5,但是通过 js 的charCodeAt()函数只能得出高位码对,但是这并不影响解码软件对字符编码进行定位,因为这些字符的代理码对都是成对地分布在  UD800-UDFFF 区段内的,并不存在交叉的现象,知道高位码对也可以简单地搜索到低位码对。

('𪚥').charCodeAt(0).toString(16) //d869

document.write('\ud869\udea5'); // 𪚥

//我们可以通过UTF-8编码来推算出这个字符的 unicode 编码
parseInt(encodeURI('𪚥').split('%').slice(1).map(function(v){ return (Array(8).join(0)+parseInt(v,16).toString(2)).slice(-8) }).join().replace(/^1{2,}0|\,10/g,''),2).toString(16);// 2a6a5

 

另外,使用代理码对表示的字符,在js字符串的 length 属性中会返回 2 。

"𪚥".length//2

"𪚥".split("").forEach(function(s){
 console.log(s.charCodeAt(0).toString(16)) // d869, dea5
})

 

 

UTF-16 编码出现以前,UD800-UDFFF 区段的码位可能会被一些计算机产品设计者利用,
而且其他位面的字符极少用到,因此,一些软件可能无法正确识别代理码对,这可能会导致一些BUG。例如,python 2.6 在 UNIX 平台上便无法正确识别代理码对。如果一个软件声称自己支持UCS-2,那么他很可能是不支持UTF-16的。

 

javascript 跟 java 一样使用UTF-16编码,因此, 实际上 javascript 程序中变量名和函数名可以使用ASCII 之外的字符,例如中文,不过网页文件保存的编码格式要注意,使用的编码格式对字符编码的范围应当不小于 UTF-16,比如保存为 utf-8 编码。

function 试试看(){
var 打个招呼 = {你好:'好你妹!'};
alert(打个招呼.你好);
}
试试看(); 



二、字符编码格式及其在 javascript 中的使用
1、unicode 16进制编码
unicode 16 使用 16位二进制编码字符,但是其编码格式在书面上使用16进制(二进制写起来太长了),在javascript中, \u 加 4个16进制字符表示一个字符的编码(每个字节 8 位二进制对应2位十六进制,2^8 = 256 = 16^2),不足4位16进制的,高位用0补足,比如 \u55B5 表示汉字 "喵",字母 "a" 的 ASCII 码是10进制 97,表示成 16 进制 unicode 编码格式就是 \u0061
试试打印出来:
document.write('\u55B5');
document.write('\u0061')

2、javascript charCodeAt 和 String.fromChartCode 使用 10 进制编码
在 javascript 字符串的 charCodeAt 和 String.fromChartCode 中取得和使用的字节编码都是 10 进制的,因此在 document.write 和 这些方法配合使用时需要进行进制转换。
另外要注意的是,如果一个变量保存了一个字符的 unicode() 编码,你想用 document.write() 打印到页面上就需要注意了,不要将'\u' 转义成 '\\u' ,如果转义了,需要使用 eval() 来执行,否则将直接把编码打印出来:

var code1 = '\\u0061';
document.write(code1); // \u0061
var code2 = '\u0061';
document.write(code2); // a


但是在表达式中,也许你想拼接出 unicode 编码后打印字符串,这就要注意了,因为在字符串中 \u 后面必须接 4个十六进制字符才是合法的语法,所以不得不转义:

var code = "\\u"+("0000"+('a').charCodeAt(0).toString(16)).slice(-4);
document.write(code); // \u0061
document.write(eval('"'+code+'"')); //正确做法 ,注意eval 时加上引号,因为 document.write 接受的参数是字符串,document.write("\u0061"),其中 \u0061 是单个字符,而不是可以分割的多个字符组成的字符串 (  "\\"+"u"+"0061" ), 而形如document.write(\u0061) 的语句是个语法错误.

 试试下面代码

function \u0061(){ console.log(123) }
\u0061();
a();

 



3、javascript 中的单字节编码
在 js 中,可以使用 \x 加 2位16进制字符标记一个单字节的字符,例如字符 'a' 可以表示为 \x61,用法类似\u 加4位16进制编码。
document.write('\x61');// a

\u 加四位十六进制数 或 \x 加2位十六进制数属于转义字符,在 js 字符串长度中只算 1 个,
转义字符不能直接用于 HTML 文件(不会转换后输出,而是直接输出转义格式的字符串),但可以用 document.write()打印出来,因为在 js 语法范围内,虽然表达方式不一样,但是转义字符和直接的字符字面量都是指同一个东西:

console.log("\u0061"==='a'); //true

 

('123|u55b5abc').length //12
('123|u55b5abc').split('') //["1", "2", "3", "|", "u", "5", "5", "b", "5", "a", "b", "c"]
('123\u55b5abc').length //7
('123\u55b5abc').split('') // ["1", "2", "3", "喵", "a", "b", "c"]

 


4、 xss 与 字符编码

当向页面显示用户输入的内容是,通常我们要过滤或转义 script 标签以避免 XSS 攻击,

但是,需要注意的是, \u+16进制 、\x+16进制 不能在html中自动转换(它们用于js程序),但是字符实体(它用于html)可以,如果出现在模板中的 HTML 标签属性中就需要特别注意了,他们会正确解码后才赋给标签的属性,例如:

document.body.innerHTML = '<img src="wrongUrl.gif" onerror="&#116;&#104;&#105;&#115;&#46;&#115;&#114;&#99;&#61;&#39;&#104;&#116;&#116;&#112;&#58;&#47;&#47;&#119;&#119;&#119;&#46;&#120;&#115;&#115;&#46;&#99;&#111;&#109;&#39;" >';

 

对标签属性之进行转义时,需要对注意,比较下面2个做法:

document.body.innerHTML = '<a onclick="\&\#116;\&\#104;\&\#105;\&\#115;">click me</a>';

document.body.innerHTML = '<a onclick="\\&\\#116;\\&\\#104;\\&\\#105;\\&\\#115;">click me</a>';

 

5、八进制转义字符

js 字符串中,\ 开始后接正数,可能被解析为8进制转义字符。

一个八进制转义字符形成的条件是:斜线后面接的整数最长3位,至少1位,单个数字的字面值不大于8。

如果过不满足任意一项,都将结束一个字符的解释,开始新字符的解释。八进制转义字符的10进制数字字面值最大值为377(即10进制的255),即 '\377' 被解析为一个字符,'\378' 被解析为2个字符: '\37' 和 '8' 

'\377'.length   //1
console.log('\377')   // ÿ
'\378'.length //2
'\128'.length //2

console.log('\127') //W 
'W'.charCodeAt(0) //87
(87).toString(8) //'127'

  


三、UTF-8 编码
由于 unicode 编码的前 128 位是ASCII 码,他们已经包含大部分西欧语言的字符,而且只要一个字节就可以全部编号了,还多出来 128 个空位,8位2进制,其实只使用了7位,第8位被设置为0,事实上,电子邮件使用的base64编码就认为第8位是1的的字符编码都是传输错误。
对于西欧语言来说,所有字符都使用 2个字节来标记太浪费计算机内存了,于是有了UTF-7、UTF-8 等变长编码方式,其中 UTF-8 又称万国码,使用最广泛,现在已经标准化为RFC 3629。

UTF-8的编码规则有二条:
1) 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2) 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode 二进制码,如果空位多于实际的unicode 二进制码位,在高位补0。
因为多字节字符编码要使用前置标记位,UFT-8 对汉字的编码可能会超出2个字节达到3个字节,更少用的字符可能达到4个字节的编码。

下表是UTF-8 编码的码位分布情况,其中的 x 表示字符的 unicode 二进制编码
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编码十六进制格式是 E596B5,把他们转换二进制:
parseInt('E5',16).toString(2) // 11100101
parseInt('96',16).toString(2) // 10010110
parseInt('B5',16).toString(2) // 10110101
因此 它的 UTF-8 二进制编码为 11100101 10010110 10110101,
其中的 二进制 unicode 编码就是 01010101 10110101
转换为 16进制编码格式就是
parseInt('0101010110110101',2).toString(16) //55b5
因此它的 unicode-16 的16进制编码是 55b5 ,试打印出来
document.write('\u55b5'); // 喵

四、 URL 编码函数: escape(已经不推荐使用) 、encodeURI、encodeURIComponent
这些函数不会对 ASCII 字母和数字进行编码,但会对一些特殊符号和多字节字符进行16进制编码,其中 escape 使用unicode-16 编码,encodeURI、encodeURIComponent 使用 UTF-8编码。
他们对 单字节 ASCII 特殊符号编码结果都是一样的,格式是 % + 2位16进制编码,例如空格被编码为 %20
对于多字节字符,escape 的编码格式为 %u + unicode 十六进制双字节编码( 如果是单字节字符则是 % + unicode + 十六进制编码),而 encodeURI、encodeURIComponent 则是在字符的每个 UTF-8 编码字节前加 % ,例如 汉字 '喵' 的编码分别是:
escape('喵'); // %u55B5
encodeURI('喵'); // %E5%96%B5
encodeURIComponent('喵'); // %E5%96%B5

encodeURI、encodeURIComponent 的区别是:
encodeURI 不对在URL 上使用的特殊含义的字符编码,如 ;/?:@&=+$,# , 所以你可以使用 encodeURI 对一个URL编码后仍能正确使用它访问网络资源或文件目录,
encodeURIComponent 的编码对象是 url 上的组件,例如附加参数,他会对 ;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号 进行编码,因此不应该使用它对整个 URL 进行编码。

五、字符实体中的 unicode
HTML 字符实体可以直接用于HTML文件中,写法为 '&#' +unicode 编码的十进制数字 + ';'
或者 '&#x' +unicode 编码的十六进制数字 + ';'
如 大于号 > 的字符实体 为 &#62; 或者 &#x003E; 高位可以不补0,也可以写成 &#x3E;
(62).toString(16) //3e
document.write('&#x003E;'); // 打印出 大于号 >

下面是HTML中常用的字符实体表,其中只有少部分有专门用于 HTML 文件输出的实体名,任何 unicode-16 字符都可以通过字符实体在 HTML 文件中打印出来。
http://www.w3school.com.cn/tags/html_ref_entities.html

六、正则表达式中的 unicode 编码

var str = 'asd乃阿当123安迪asd123啊那的';
str.match(/[\u0000-\u00ff]/g); //单字节字符
//["a", "s", "d", "1", "2", "3", "a", "s", "d", "1", "2", "3"]

str.match(/[\x00-\xff]/g); //单字节字符
//["a", "s", "d", "1", "2", "3", "a", "s", "d", "1", "2", "3"]

str.match(/[\u0100-\uffff]/g); //双字节字符
//["乃", "阿", "当", "安", "迪", "啊", "那", "的"]

 



七、大端通讯、小端通讯和BOM(Byte Order Mark)
在数据通讯中传输数据时,可以采用两种数据传输方式:高位在前(大端,big endian)或高位在后(小端,little endian).
例如汉字'喵' 的unicode 16进制编码为 16进制数 0x55B5 ,传输方式:
大端通讯:0x55 0xB5
小端通讯:0xB5 0x55

BOM 是针对单个字符的本身的多字节编码而言的,如果字符的编码都是使用一个字节来编码则不存在字节序的问题,例如 ASCII 编码就不会存在字节序问题。
接收的二进制数据按每8位1字节,每字节使用2个十六进制字符编码为 unicode 后,存在一个问题,如何区别是高位在前还是高位在后。
Unicode规范中有一个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且,编码为FFFE 的字符在 unicode 中不存在,因此在存储文件或在网络传输字节流时,可以用这个字符来标记大端通讯和小端通讯。
UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。
在存储文件时,如果一个文本文件的头两个字节是FE FF,就表示该文件采用大端方式;如果头两个字节是FF FE,就表示该文件采用小端方式。

由于UTF-8 多字节编码方式定义其第一个字节的前几位使用 1 来声明该字符占多少个字节,因此,UTF-8编码方式不存在编码字节顺序的问题,但是仍然可以给 UTF-8 字节数据加上大端小端标记(带BOM 的UTF-8 编码格式)。 BOM 对于 UTF-8 格式文件是没实用意义的,因此一般推荐保存为不带 BOM 的UTF-8 格式。

javascript 的类型数组在读取二进制数据(BLOB)时,是以 小端方式存入数组的,相关资料参考下面:

http://www.cnblogs.com/ecalf/archive/2012/11/25/2787219.html


关于BOM ,可访问下面链接进一步了解
http://www.cppblog.com/colorful/archive/2012/05/17/175182.html

八、中文编码 GB2312 、GBK、BIG5
这些编码不是unicode 编码而是区位码和汉字内码,这些编码前一个字节仍然兼容ASCII,GB2312(简体) 、GBK(简繁体)由中国大陆机构编撰,BIG5(繁体)由港奥台相关机构和计算机厂商编撰,区位码在 windows 平台使用的代码页转换表(CodePage)映射到 unicode 编码。
代码页技术现在已经广泛为各种平台所采用。
UTF-7的代码页是65000,UTF-8的代码页是65001。

 

 

九、维基百科中关于 utf-16 代理码对算法的描述:

从U+10000到U+10FFFF的码位

辅助平面(Supplementary Planes)中的码位,在UTF-16中被编码为一对16比特长的码元(即32bit,4Bytes),称作 code units called a 代理对(surrogate pair), 具体方法是:

UTF-16解码
lead \ trail DC00DC01   …   DFFF
D800 10000 10001 103FF
D801 10400 10401 107FF
  ⋮
DBFF 10FC00 10FC01 10FFFF
  • 码位减去0x10000, 得到的值的范围为20比特长的0..0xFFFFF.
  • 高位的10比特的值(值的范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理(high surrogate), 值的范围是0xD800..0xDBFF. 由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates).
  • 低位的10比特的值(值的范围也是0..0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low surrogate), 现在值的范围是0xDC00..0xDFFF. 由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates).

由于高位代理、低位代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的: 一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing): 可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元

 

计算过程:

//以为 字符 𪚥 为例,其 unicode 16进制编码 2A6A5,unicode 编码的二进制码位数:(0xFFFFF).toString(2).length==20
var c =  (Array(20).join(0)+parseInt(0x2A6A5-0x10000).toString(2)).slice(-20);
var c1 = c.slice(0,10);
var c2 = c.slice(10,20);
console.log(c,'|',c1,'|',c2);

var l = (parseInt(c1,2)+0xd800).toString(16);
var t = (parseInt(c2,2)+0xDC00).toString(16);
var code = '\\'+'u'+l+'\\'+'u'+t;

document.write(eval("'"+code+"'")); //𪚥

 

 

posted @ 2012-09-04 07:20 ecalf 阅读(...) 评论(...) 编辑 收藏