String.prototype.charAt()
charAt()方法返回指定位置的字符。
返回指定位置字符
var anyString = "Brave new world"; console.log("The character at index 0 is '" + anyString.charAt(0) + "'"); console.log("The character at index 1 is '" + anyString.charAt(1) + "'"); console.log("The character at index 2 is '" + anyString.charAt(2) + "'"); console.log("The character at index 3 is '" + anyString.charAt(3) + "'"); console.log("The character at index 4 is '" + anyString.charAt(4) + "'"); console.log("The character at index 999 is '" + anyString.charAt(999) + "'"); //The character at index 0 is 'B' //The character at index 1 is 'r' //The character at index 2 is 'a' //The character at index 3 is 'v' //The character at index 4 is 'e' //The character at index 999 is ''
unicode相关知识
- 2^16(65536)个号码组成一个平面(plain)
- 目前一共17个平面
- 1个基本平面(BMP),号码范围是U+0000 ~U+FFFF
- 16个辅助平面(SMP),和号码范围是U+010000 ~U+10FFFF
UTF-32由4个字节表示一个字符,但是占空间较大,浪费空间。
UTF-8是变长编码,字符长度由1个字节到4个字节不等。
UTF-16中基本平面的字符占用2个字节,辅助平面的字符占用4个字节。也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。
在基本平面内,从U+D800到U+DFFF是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。
辅助平面的字符位共有2^20个,也就是说,对应这些字符至少需要20个二进制位。UTF-16将这20位拆成两半,前10位映射在U+D800到U+DBFF(空间大小210),称为高位(H),后10位映射在U+DC00到U+DFFF(空间大小210),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
这就是高半区和低半区。D800-DBFF,高半区;DC00-DFFF,低半区。
所以,当我们遇到两个字节,发现它的码点在U+D800到U+DBFF之间,就可以断定,紧跟在后面的两个字节的码点,应该在U+DC00到U+DFFF之间,这四个字节必须放在一起解读。
下面是辅助平面字符转换成基本平面字符公式:
H = Math.floor((c-0x10000) / 0x400)+0xD800
L = (c - 0x10000) % 0x400 + 0xDC00
遍历字符串的时候,必须对码点做一个判断,只要落在0xD800到0xDBFF的区间,就要连同后面2个字节一起读取。
类似的问题存在于所有的JavaScript字符操作函数。
while (++index < length) { // ... if (charCode >= 0xD800 && charCode <= 0xDBFF) { output.push(character + string.charAt(++index)); } else { output.push(character); } }
String.prototype.replace(),String.prototype.substring(),String.prototype.slice()等等的函数都存在这个问题。它们都只对2字节的码点有效。要正确处理4字节的码点,就必须逐一部署自己的版本,判断一下当前字符的码点范围。
非基础平面字符正确显示
通过一系列操作后可以正确显示非基础平面的4字节字符:
var str = 'A \uD87E\uDC04 Z'; for (var i=0, chr; i < str.length; i++) {//循环字符串 if ((chr = getWholeChar(str, i)) === false) { continue; } alert(chr); } function getWholeChar (str, i) {//此函数获取正确字符,str是当前循环到的字符,i是在字符串中的位置 var code = str.charCodeAt(i);//获取当前字符的在基本平面BMP的UTF-16编码位置 if (isNaN(code)) {//如果charCodeAt返回了NaN那就return return ''; } if (code < 0xD800 || code > 0xDFFF) {//如果当前字符编码既不在高半区也不在低半区 return str.charAt(i);//说明当前字符是2字节的基础平面普通字符,那就直接返回该字符 } if (0xD800 <= code && code <= 0xDBFF) {//如果当前字符编码在高半区,说明紧跟着的下一个字符应该是在低半区 if (str.length <= (i+1)) {//如果没有下一个低半区字符就抛出错误 throw 'High surrogate without following low surrogate'; } var next = str.charCodeAt(i+1);//获取到紧跟着字符的编码 if (0xDC00 > next || next > 0xDFFF) {//如果紧跟的字符不在低半区,就抛出错误 throw 'High surrogate without following low surrogate'; } return str.charAt(i)+str.charAt(i+1);//将两个unicode字符连起来然后返回 } if (i === 0) {//如果当前字符是低半区且是第一个字符,就抛出错误,因为它前面缺少高半区的字符 throw 'Low surrogate without preceding high surrogate'; } var prev = str.charCodeAt(i-1);//否则就回去它前面的字符的编码 if (0xD800 > prev || prev > 0xDBFF) {//如果它前面的字符不在高半区内,抛出错误 throw 'Low surrogate without preceding high surrogate'; } return false; }
在允许解构分配的独占JavaScript 1.7+环境(如Firefox)中,以下是一个更简洁和更灵活的替代方法,它会自动递增一个递增变量(如果字符保证它是一个替代对)。
var str = 'A\uD87E\uDC04Z'; for (var i=0, chr; i < str.length; i++) { [chr, i] = getWholeCharAndI(str, i); alert(chr); } function getWholeCharAndI (str, i) {//str是当前字符,i是索引 var code = str.charCodeAt(i);//获取当前字符的编码 if (isNaN(code)) {//如果NaN就返回 return ''; } if (code < 0xD800 || code > 0xDFFF) {//既不在高半区也不在低半区说明是普通字符,i不变化 return [str.charAt(i), i]; } if (0xD800 <= code && code <= 0xDBFF) {//如果在高半区 if (str.length <= (i+1)) {//如果缺少紧跟的低半区字符就抛错误 throw 'High surrogate without following low surrogate'; } var next = str.charCodeAt(i+1);//获取紧跟的字符的编码 if (0xDC00 > next || next > 0xDFFF) {//如果紧跟的字符不在低半区内就抛出错误 throw 'High surrogate without following low surrogate'; } return [str.charAt(i)+str.charAt(i+1), i+1];//返回,i变成了i+1 } if (i === 0) {//如果在低半区且缺少前一个高半区字符,就抛出错误 throw 'Low surrogate without preceding high surrogate'; } var prev = str.charCodeAt(i-1);//获取前一个字符的编码 if (0xD800 > prev || prev > 0xDBFF) {//如果前一个字符不在高半区内,抛出错误 throw 'Low surrogate without preceding high surrogate'; } return [str.charAt(i+1), i+1]; }
修复charAt()以支持非基础平面BMP字符
function fixedCharAt (str, idx) {//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是低半区的低半区字符就+1跳过 idx++; } else { break; } } if (idx >= end || idx < 0) {//如果idx不在字符串长度范围内,返回空字符串 return ''; } ret += str.charAt(idx); if (/[\uD800-\uDBFF]/.test(ret) && /[\uDC00-\uDFFF]/.test(str.charAt(idx+1))) { ret += str.charAt(idx+1); } return ret; }
实现简单的charAt():
String.prototype.charAt = function () { var temp = Number(arguments[0]); if (Number.isNaN(temp)) { return this[0]; } var condition = Object.prototype.toString.call(temp) == '[object Number]'; if (condition && (temp < 0 || temp > this.length)) { return ''; } else { return this[parseInt(temp)]; } }