最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 你还在用charCodeAt那你就out了

    正文概述 掘金(十年踪迹)   2020-12-03   860

    在 JavaScript 中处理中文和其他 Unicode 字符时,我们会用到处理 Unicode 相关的 API。

    在早期,JavaScript 提供的String.prototype.charCodeAtString.fromCharCode就是能够将字符串转换为 Unicode 的 UTF-16 编码以及从 UTF-16 编码转换为字符串的函数。

    比如:

    const str = '中文';
    
    console.log([...str].map(char => char.charCodeAt(0)));
    // [20013, 25991]
    
    

    这里我们将字符串展开成单个字符,再通过 charCodeAt 方法将字符串转换为对应的 Unicode 编码,这里的 20013 和 25991 就是 “中文” 两个字对应的 Unicode 编码。

    同样,我们可以使用 fromCharCode 将 Unicode 编码转换为字符串:

    const charCodes = [20013, 25991];
    
    console.log(String.fromCharCode(...charCodes)); // 中文
    
    

    这两个方法相信大部分同学都不陌生,这是从 ES3 就开始支持的方法。但是,这个方法在今天我们处理 Unicode 字符时不够用了。

    为什么呢?我们来看一下例子:

    const str = '?';
    
    console.log(str.charCodeAt(0)); // 55356
    
    

    这个字符是我们熟悉的麻将中的红中,现在很多输入法都能直接打出来,看上去似乎也正常,没什么问题啊?

    可你再试试:

    console.log(String.fromCharCode(55356)); // �
    
    

    实际上 Unicode 字符?的 UTF-16 编码并不是 55356,这时候如果你使用 charCodeAt 来得到字符?的 UTF-16 编码,应该要到两个值:

    const str = '?';
    
    console.log(str.charCodeAt(0), str.charCodeAt(1)); // 55356 56324
    
    

    对应的String.fromCharCode(55356, 56324)才能还原?字符。

    除此以外,还有其他一些不一样的地方,比如:

    console.log('?'.length); // 字符串长度为2
    '?'.split(''); // ["�", "�"] split 出来两个字符
    /^.$/.test('?'); // false
    
    

    ??知识点:Unicode 标准中,将字符编码的码位以2**16个为一组,组成为一个平面(Plane),按照字符的码位值,分为 17 个平面,所有码位从 0x000000 到 0x10FFFF,总共使用 3 个字节。

    其中最前面的 1 个字节是平面编号,从 0x0 到 0x10,一共 17 个平面。

    第 0 号平面被称为基本多文种平面(BMP,Basic Multilingual Plane),这个平面的所有字符码位只需要 16 位编码单元即可表示,所以它们可以继续使用 UTF-16 编码。

    其他的平面被称为辅助平面(supplementary plane),这些平面的字符被称为增补字符,它们的码位均超过 16 位范围。

    ES5 及之前的 JavaScript 的 Unicode 相关 API,只能以 UTF-16 来处理 BMP 的字符,所有字符串的操作都是基于 16 位编码单元。

    因此,当?这样的增补字符出现时,得到的结果就会与预期不符。

    在 ES2015 之后,JavaScript 提供了新的 API 来支持 Unicode 码位,所以我们可以这么使用:

    const str = '?';
    
    console.log(str.codePointAt(0)); // 126980
    
    

    ?? 知识点String.prototype.codePointAt(index) 方法返回字符串指定 index 位置的字符的 Unicode 码位,与旧的 charCodeAt 方法相比,它能够很好地支持增补字符。

    对应地,我们有String.fromCodePoint方法将 CodePoint 转为对应的字符:

    console.log(String.fromCodePoint(126980)); // ?
    
    

    Unicode 转义

    JavaScript 字符串支持 Unicode 转义,所以我们可以用码位的十六进制字符串加上前缀\u来表示一个字符,例如:

    console.log('\u4e2d\u6587'); // 中文
    
    

    0x4e2d0x6587分别是 20013 和 25991 的十六进制表示。

    注意,Unicode 转义不仅仅可以用于字符串,实际上 \ uxxxx 也是可以用在标识符,并相互转换的。例如我们可以这么写:

    const \u4e2d\u6587 = '测试';
    
    console.log(中文); // 测试
    
    

    上面的代码我们定义了一个中文变量,声明的时候我们用 Unicode 转义,console.log 的时候用它的变量名字符,这样也是没有问题的。

    \u 和十六进制字符的这种表示法同样只适用于 BMP 的字符,所以如果我们试图使用它转义增补字符,直接这样是不行的:

    console.log('\u1f004'); // ὆4
    
    

    这样,引擎会把\u1f004解析成字符\u1f00和阿拉伯数字 4 组成的字符串。我们需要使用{}将编码包含起来,这样就可以了:

    console.log('\u{1f004}'); // ?
    
    

    代理对(surrogate pair)

    为区别 BMP 来表示辅助平面,Unicode 引入代理对 (surrogate pair),规定用 2 个 16 位编码单元来表示一个码位,具体规则是将一个字符按如下表示:

    • 在 BMP 内的字符,仍然按照 UTF-16 的编码规则,使用两个字节来表示。
    • 增补字符使用两组 16 位编码来表示一个字符规则为:
      • 首先将它的编码减去 0x10000
      • 然后写成 yyyy yyyy yyxx xxxx xxxx 的 20 位二进制形式
      • 然后编码为 110110yy yyyyyyyy 110111xx xxxxxxxx 一共 4 个字节。

    其中 110110yyyyyyyyyy 和 110111xxxxxxxxxx 就是两个代理字符,形成一组代理对,其中第一个代理字符的范围从 U+D800 到 U+DBFF,第二个代理字符的范围从 U+DC00 到 U+DFFF。

    实现 getCodePoint

    理解了代理对,我们就可以通过 charCodeAt 实现 getCodePoint 了:

    function getCodePoint(str, idx = 0) {
      const code = str.charCodeAt(idx);
      if(code >= 0xD800 && code <= 0xDBFF) {
        const high = code;
        const low = str.charCodeAt(idx + 1);
        return ((high - 0xD800) * 0x400) +
          (low - 0xDC00) + 0x10000;
      }
      return code;
    }
    
    console.log(getCodePoint('中')); // 20013
    console.log(getCodePoint('?')); // 126980
    
    

    同样地,我们也可以通过 fromCharCode 实现 fromCodePoint:

    function fromCodePoint(...codePoints) {
      let str = '';
      for(let i = 0; i < codePoints.length; i++) {
        let codePoint = codePoints[i];
        if(codePoint <= 0xFFFF) {
          str += String.fromCharCode(codePoint);
        } else {
          codePoint -= 0x10000;
          const high = (codePoint >> 10) + 0xD800;
          const low = (codePoint % 0x400) + 0xDC00;
          str += String.fromCharCode(high) + String.fromCharCode(low);
        }
      }
      return str;
    }
    
    console.log(fromCodePoint(126980, 20013)); // ?中
    
    

    所以我们就可以用上面这样的思路来实现早期浏览器下的 polyfill。实际上 MDN 官方对 codePointAt 和 fromCodePoint 的说明中,就按照上面的思路提供了对应的 polyfill 方法。

    getCodePointCount

    JavaScript 字符串的 length 只能获得 UTF-16 字符的个数,所以前面看到的:

    console.log('?'.length); // 字符串长度为2
    
    

    要获得 Unicode 字符数,有几个办法,比如使用 spread 操作是可以支持 Unicode 字符串转数组的,所以:

    function getCodePointCount(str) {
      return [...str].length;
    }
    console.log(getCodePointCount('?中'));
    
    

    或者使用带有 u 描述符的正则表达式:

    function getCodePointCount(str) {
      let result = str.match(/./gu);
      return result ? result.length : 0;
    }
    console.log(getCodePointCount('?中'));
    
    

    扩展

    Unicode 码位使用固定的 4 个字节来编码增补字符,而早期,UTF-8 编码则采用可变的 1~6 个字节来编码 Unicode 字符。

    UTF-8 编码方式如下:

    字节起始终止byte1byte2byte3byte4byte5byte6
    1U+0000U+007F0xxxxxxx2U+0080U+07FF110xxxxx10xxxxxx3U+0800U+FFFF1110xxxx10xxxxxx10xxxxxx4U+10000U+1FFFFF11110xxx10xxxxxx10xxxxxx10xxxxxx5U+200000U+3FFFFFF111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx6U+4000000U+7FFFFFFF1111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx

    在浏览器的 encodeURIComponent 和 Node 的 Buffer 默认采用 UTF-8 编码:

    console.log(encodeURIComponent('中')); // %E4%B8%AD
    
    
    const buffer = new Buffer('中');
    console.log(buffer); // <Buffer e4 b8 ad>
    
    

    这里的 E4、B8、AD 就是三个字节的十六进编码,我们试着转一下:

    const byte1 = parseInt('E4', 16); // 228
    const byte2 = parseInt('B8', 16); // 184
    const byte3 = parseInt('AD', 16); // 173
    
    const codePoint = (byte1 & 0xf) << 12 | (byte2 & 0x3f) << 6 | (byte3 & 0x3f);
    
    console.log(codePoint); // 20013
    
    

    我们将三个字节的控制码 1110、10、10 分别去掉,然后将它们按照从高位到低位的顺序拼接起来,正好就得到'中'的码位 20013。

    所以我们也可以利用 UTF-8 编码规则,写另一个版本的通用方法来实现 getCodePoint:

    function getCodePoint(char) {
      const code = char.charCodeAt(0);
      if(code <= 0x7f) return code;
      const bytes = encodeURIComponent(char)
        .slice(1)
        .split('%')
        .map(c => parseInt(c, 16));
      
      let ret = 0;
      const len = bytes.length;
      for(let i = 0; i < len; i++) {
        if(i === 0) {
          ret |= (bytes[i] & 0xf) << 6 * (len - i - 1);
        } else {
          ret |= (bytes[i] & 0x3f) << 6 * (len - i - 1);
        }
      }
      return ret;
    }
    
    console.log(getCodePoint('中')); // 20013
    console.log(getCodePoint('?')); // 126980
    
    

    那么同样,我们可以实现 fromCodePoint:

    function fromCodePoint(point) {
      if(point <= 0xffff) return String.fromCharCode(point);
      const bytes = [];
      bytes.unshift(point & 0x3f | 0x80);
      point >>>= 6;
      bytes.unshift(point & 0x3f | 0x80);
      point >>>= 6;
      bytes.unshift(point & 0x3f | 0x80);
      point >>>= 6;
      if(point < 0x1FFFFF) {
        bytes.unshift(point & 0x7 | 0xf0);
      } else if(point < 0x3FFFFFF) {
        bytes.unshift(point & 0x3f | 0x80);
        point >>>= 6;
        bytes.unshift(point & 0x3 | 0xf8);
      } else {
        bytes.unshift(point & 0x3f | 0x80);
        point >>>= 6;
        bytes.unshift(point & 0x3f | 0x80);
        point >>>= 6;
        bytes.unshift(point & 0x1 | 0xfc);
      }
      const code = '%' + bytes.map(b => b.toString(16)).join('%');
      return decodeURIComponent(code);
    }
    
    console.log(fromCodePoint(126980)); // ?
    
    

    关于 Unicode,你还有什么想讨论的,欢迎在 issue 中留言。


    起源地下载网 » 你还在用charCodeAt那你就out了

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元