charCodeAt() and codePointAt()

这篇文章主要介绍javascript里的charCodeAt()方法和ES6引进的codePointAt()方法.

在说这两个方法之前,必须要先补充一些关于Unicode编码的知识…

ASCII码是256(2^8)个(0-255)数字,每一个数字都是一个码点(code point)[1].用于代表256个字符.但是这256个字符只是常用字符,各国语言,包括中文,远远超过这个范围.所以Unicode就出现了,Unicode源于一个很简单的想法:将全世界所有的字符包含在一个集合里,计算机只要支持这一个字符集,就能显示所有的字符,再也不会有乱码了。

可以这么理解,Unicode码是ASCII码的一个超集(superset).它的前128个code point和ASCII是一致的.后面又增加了很多很多字符.而UTF-32,UTF-8,UTF-16,都是Unicode码的编码形式:

UTF-32
用固定长度的四个字节来表示每个码点,就算有些字符只需要一个字节,也通过把空缺位添加0来强制转换成8个字节.由于四个字节有32-bit,所以称为UTF-32编码.由于太浪费空间,比ASCII编码文件要大四倍,所以不能使用.

UTF-8
用可变长度的字节来表示每个码点,如果只需要一个字节就能表示的,就用一个字节,一个不够,就用两个…所以,在UTF-8编码下,一个字符有可能由1-4个字节组成.

UTF-16
结合了固定长度和可变长度,它只有两个字节和四个字节两种方式来表示码点.如果是两个字节,那么码点的范围应该是(U+0000到U+FFFF),如果是四个字节,那么码点的范围应该是(U+010000到U+10FFFF).
但是注意,我们不能直接使用 U+010000 这样的形式(应该是因为位数不同所以不能…),所以,四个字节的码点需要使用两个 code unit [2],来组成一个 surrogate pair [3]. 其中第一个code unit的范围是从 0xD8000xDBFF,被成为高位(high surrogate)或头位(lead surrogate),第二个code unit的范围是从 0xDC000xDFFF,被称为低位(low surrogate)或尾位(trail surrogate).

好了,有了以上的这些知识,就可以看看 charCodeAt()codePointAt()了.

charCodeAt()

charCodeAt()方法返回一个0-65535的整数,这个数代表了指定索引位置的UTF-16 code unit的码点.(UTF-16 code unit匹配的码点有两种可能,一种是这个码点本身可以代表一个字符,另一种是,这个code unit可能是一组 surrogate pair 里的一个code unit,它的码点本身不代表任何字符,比如 Unicode码点范围 > 0x10000 的那些).如果你需要的是整个 surrogate pair 的码点值,请使用 codePointAt*() 方法.

语法

str.charCodeAt(index)

参数

index

一个大于等于0,小于字符串长度的整数; 如果它不是一个数字,默认为0.

描述

Unicode 码点的范围是 0-1114111 (0x10FFFF). 最前面的128个Unicode码点和ASCII码指向的字符完全相同.

注意, charCodeAt() 总是返回一个小于65536的值,这是因为,大于65536的码点会使用两个小于65536的’surrogate’伪字符来组成一个真实字符.因此,为了检查或重现这类字符的整个码点(大于等于65536),不仅需要使用 charCodeAt(i) 来检索,还需要使用 charCodeAt(i+1),这就好像通过两个字母来检查重现一个字符串那样. 或者也可以使用 codePointAt(i). 查看下面的例子2和3.

如果索引值小于0或者大于等于字符串的长度,charCodeAt() 返回 NaN.

注意,字符串的长度并不一定是你看到的字符的长度,有些中文字,比如‘ji’,虽然只有一个字,但它的length是2.它就是Unicode码点大于等于65536的一个例子.

向下兼容: 在历史版本(比如JavaScript 1.2), charCodeAt() 方法返回对应索引位置字符指向 ISO-Lation-1 字符集的一个数字. ISO-Latin-1 字符集的范围是 0 到255.其中0-127直接映射ASCII码.

例子

使用 charCodeAt()

下面这个例子返回65, ‘A’的Unicode码点.

'ABC'.charCodeAt(0); // returns 65

改造charCodeAt()来处理当一开始并不知道非基本平面字符(non-Basic-Multilingual-Plane) [4]的高位在哪个位置出现的情况:

这个版本可以用在循环或者类似的场景:一开始并不知道是否有非基本平面字符存在于指定的索引位置前.

function fixedCharCodeAt(str, idx) {
  // ex. fixedCharCodeAt('\uD800\uDC00', 0); // 65536
  // ex. fixedCharCodeAt('\uD800\uDC00', 1); // false
  idx = idx || 0;
  var code = str.charCodeAt(idx);
  var hi, low;

  // 如果索引位置是一个高位 (可以把后面的十六进制改成0xDB7F来把高位当成一个单独的字符)
  if (0xD800 <= code && code <= 0xDBFF) {
    hi = code;
    low = str.charCodeAt(idx + 1);
    if (isNaN(low)) {
      throw 'High surrogate not followed by low surrogate in fixedCharCodeAt()';
    }
    return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
  }
  if (0xDC00 <= code && code <= 0xDFFF) { // 低位
    // 返回false以跳出循环的本次迭代,因为在上次迭代时,遇到和它配对的那个高位时,已经处理过它了(处理的过程就是注释掉的那段)
    return false;
    /*hi = str.charCodeAt(idx - 1);
    low = code;
    return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;*/
  }
  return code;
}

改造charCodeAt()来处理当一开始就已经知道非基本平面字符(non-Basic-Multilingual-Plane) [4]的高位在哪个位置出现的情况:

function knownCharCodeAt(str, idx) {
  str += '';
  var code,
      end = str.length;

  var surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
  while ((surrogatePairs.exec(str)) != null) {
    var li = surrogatePairs.lastIndex;
    if (li - 2 < idx) {
      idx++;
    }
    else {
      break;
    }
  }

  if (idx >= end || idx < 0) {
    return NaN;
  }

  code = str.charCodeAt(idx);

  var hi, low;
  if (0xD800 <= code && code <= 0xDBFF) {
    hi = code;
    low = str.charCodeAt(idx + 1);
    // Go one further, since one of the "characters" is part of a surrogate pair
    return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
  }
  return code;
}

codePointAt()

codePointAt()方法返回一个Unicode码点值,它是一个非负整数,

语法

str.codePointAt(pos)

参数

pos

指定从字符串里获取Unicode码点的索引位置.

描述

如果在指定的位置没有元素,返回undefined.如果指定位置没有 UTF-16 surrogate pair, 返回指定位置的 code unit 的码点.

栗子

'ABC'.codePointAt(1);          // 非UTF-16 surrogate pair ,返回A的Unicode码点: 66
'\uD800\uDC00'.codePointAt(0); // UTF-16 surrogate pair 返回整个字符的码点: 65536

'XYZ'.codePointAt(42);         // undefined

兼容低版本浏览器(Polyfill)

下面的扩展允许你在原本不支持这个方法的低版本的浏览器中像支持ES6一样的使用codePointAt()方法.

/*! http://mths.be/codepointat v0.1.0 by @mathias */
if (!String.prototype.codePointAt) {
  (function() {
    'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
    var codePointAt = function(position) {
      if (this == null) {
        throw TypeError();
      }
      var string = String(this);
      var size = string.length;
      // `ToInteger`
      var index = position ? Number(position) : 0;
      if (index != index) { // better `isNaN`
        index = 0;
      }
      // Account for out-of-bounds indices:
      if (index < 0 || index >= size) {
        return undefined;
      }
      // Get the first code unit
      var first = string.charCodeAt(index);
      var second;
      if ( // check if it’s the start of a surrogate pair
        first >= 0xD800 && first <= 0xDBFF && // high surrogate
        size > index + 1 // there is a next code unit
      ) {
        second = string.charCodeAt(index + 1);
        if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
          // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
          return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
        }
      }
      return first;
    };
    if (Object.defineProperty) {
      Object.defineProperty(String.prototype, 'codePointAt', {
        'value': codePointAt,
        'configurable': true,
        'writable': true
      });
    } else {
      String.prototype.codePointAt = codePointAt;
    }
  }());
}

总结

上面关于charCodeAt()codePointAt()的详解分别来自于:
charCodeAt()以及codePointAt();

这里面有几个重点:

  1. Unicode 码点,基本多语言平面(Basic-Multilingual-Plane)只支持0-65535的码点,每个字符占用两个字节,对应一个码点,超过这个范围的码点,需要使用四个字节,两个 code unit,, 一个高位(U+D800到U+DBFF),一个低位(U+DC00到U+DFFF),他们组成一个 surrogate pair,代表一个字符.
  2. charCodeAt() 方法只返回对应位置的code unit对应的码点.不管这个码点是不是代表了一个字符,不管这个code unit是独立的字符还是 surrogate pair 的一部分.
  3. codePointAt() 方法返回对应位置的code unit 或 surrogate pair的码点: 遇到独立能代表字符的码点,就返回它; 遇到 surrogate pair 里的高位 code unit,就返回整个 surrogate pair 的码点; 遇到 surrogate pair 里的低位 code unit,也返回这个 code unit 对应的码点(虽然它不配合任何字符).
  4. 文章中多次用到一个公式,就是已知 surrogate pair 的高位 code unit 的码点和低位 code unit 的码点,如何求得整个 surrogate pair 的码点,这个公式来自于surrogate-formulae

最后,再给出一个综合例子,帮助理解:online site

console.log('\uD800\uDC00'.charCodeAt(0));  //55296  
console.log('\uD800\uDC00'.charCodeAt(1));  //56320  
console.log('\uD800\uDC00'.codePointAt(0)); //65536  获取的是整个 surrogate pair 的码点
console.log('\uD800\uDC00'.codePointAt(1)); //56320  获取的是'\uDC00'的码点, 同charCodeAt()   
console.log('\uD800'.codePointAt(0));       //55296  获取的是'\uD800'的码点, 同charCodeAt()


//根据阮一峰老师的文章:"当我们遇到两个字节,发现它的码点在U+D800到U+DBFF之间,就可以断定,紧跟在后面的两个字节的码点,应该在U+DC00到U+DFFF之间,这四个字节必须放在一起解读。"
//所以,'\uD800\uDC00'应该两个码点一起解读,解读的结果就是65536

console.log('𠮷'.length);  //2
console.log('𠮷'.codePointAt(0));  //134071 
console.log('𠮷'.codePointAt(1));  //57271

//𠮷(ji)这个字,是由两个码点,四个字节组成的,所以它的length是2.两个码点一起解读,得到的结果就是134071

//最后一点,当获取这类需要两个码点一起解读组成一个字符的时候,如果获取高位的code point,它得到的会是整个字符的Unicode值,但如果获取低位的code point,它得到的依然是第二个码点的Unicode值.

[1]:code point(码点) Unicode里的码点,有几种表达方式,比如 ‘©’ 符号,它的码点是 U+00A90xA9\u00A9 或十进制的 169
其中 \u00A9 是js字符串里使用的方法, 0xA9 是写js的时候使用的方法, 169 最容易理解,就是一一对应的数字嘛,和ASCII的0-255数字一样,每个数字代表一个码点.


[2]:code unit 不知道中文应该翻译成啥,编码单元? 在UTF-16编码方式时,如果是一个四字节的字符,比如上面一个土,下面一个口,念‘ji’的那个字,它的Unicode码点是 U+20BB7,变成两个字节以后应该是 \uD842\uDFB7 .在UTF-16编码里,需要由两个字节组成的字符,其中的每个字节就称为一个 code unit ,两个 code unit 组成一个 surrogate pair.


[3]:surrogate pair 不知道中文翻译成啥. 在UTF-16编码里,如有四个字节,两个 code unit 组成的字符,它就被称为 surrogate pair


[4]:Basic-Multilingual-Plane 基本多语言平面. Unicode码点是由多个平面组成的,每个平面存放65536(2^16)个码点,其中第一个平面被称为 基本多语言平面 或者 BMP. 它包含了从 U+0000 到 U+FFFF 的码点,这些是各国最常用的字符.


其他参考:阮一峰老师写的Unicode资料