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)];
    }
}

 

posted @ 2018-06-19 17:36  hahazexia  阅读(377)  评论(0)    收藏  举报