JavaScript 字符码和字节码转换 🔁

2017/07/20 | 4分钟阅读 | 更新于 2017/07/20

近日在公司研究开发小程序版应用的可行性,某天收到一段Java代码,是接口的加密解密代码,大致流程是逐字符(编码范围在33~127的,也就是说对汉字没有影响)对编码进行变换,变换的方式取决于字符串的长度,某个随机数,以及字符在字符串中的位置。那好说啊,去掉各种类型转换就好了,于是我很快搞了一版 JavaScript 的出来,试了下加密解密,OK的。于是我又拿到一个接口,写个 AJAX 请求解密下试试。嗯?嗯?乱的?(O_O)?

之后我核对了一遍我的代码,然后又整理了代码的逻辑自己写了一份,结果还是不行。之后继续确定,请求到的内容是完整的,没有被改动。也没有不该有的转义字符啊什么的。

于是我决定把那份Java代码在本地跑一遍,新建个 Hello World 的模板,按照错误提示把类塞进去,把常用的符号写个字符串,在 JS 版本和 Java 版本上都跑跑看。后来在含有汉字的字符串的时候发现了问题。打个断点发现,Java 中 byte[] bytes = str.getBytes();,这个地方的 bytes 比我 JS 中的结果长了很多。因为我 JS 代码这个地方错误的取成了 str.charCodeAt() 的值。对于汉字“中”来说,前者是字节码,而后者是字符编码。

好了,前面都是废话,下面是正文。

Java 中 String.getBytes() 获取到的内容是字符串的字节码。不指定编码的话,使用平台的默认字符集将字符串编码为 byte 序列,并将结果存储到一个新的 byte 数组中。 From: RUNOOB - Java getBytes() 方法

JavaScript 中 String.charCodeAt() 获取到的是:

返回指定位置的字符的 Unicode 值。 From: MSDN - charCodeAt 方法 (String) (JavaScript)

返回值是一表示给定索引处字符的 UTF-16 代码单元值的数字;如果索引超出范围,则返回 NaN。 From: MDN - String.prototype.charCodeAt()

charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。 方法 charCodeAt() 与 charAt() 方法执行的操作相似,只不过前者返回的是位于指定位置的字符的编码,而后者返回的是字符子串。 From: W3School - JavaScript charCodeAt() 方法

就是 JavaScript 中 String.charCodeAt() 获取到的是字符的 Unicode 字符编码,而 Java String.getBytes() 获取到的是字节码,据说编码方式如果不指定的话跟平台有关,当时用 MAC 得到的是 UTF-8,服务端 Linux 也应该是 UTF-8,当时写的时候,完全没去考虑 byte 的意义。

然后我在 StackOverflow 看到一段字符码转字节码的代码

function toUTF8Array(str) {
  var utf8 = [];
  for (var i = 0; i < str.length; i++) {
    var charcode = str.charCodeAt(i);
    if (charcode < 0x80) utf8.push(charcode);
    else if (charcode < 0x800) {
      utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
    } else if (charcode < 0xd800 || charcode >= 0xe000) {
      utf8.push(
        0xe0 | (charcode >> 12),
        0x80 | ((charcode >> 6) & 0x3f),
        0x80 | (charcode & 0x3f),
      );
    }
    // surrogate pair
    else {
      i++;
      // UTF-16 encodes 0x10000-0x10FFFF by
      // subtracting 0x10000 and splitting the
      // 20 bits of 0x0-0xFFFFF into two halves
      charcode =
        0x10000 + (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
      utf8.push(
        0xf0 | (charcode >> 18),
        0x80 | ((charcode >> 12) & 0x3f),
        0x80 | ((charcode >> 6) & 0x3f),
        0x80 | (charcode & 0x3f),
      );
    }
  }
  return utf8;
}

但是没有找到反过来的成熟轮子。于是后来决定自己看下编码方式自己写一段。

然后维基百科可查:

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。

UTF-8使用一至六个字节为每个字符编码(尽管如此,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):

码点的位数 码点起值 码点终值 字节序列 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6
7 U+0000 U+007F 1 0xxxxxxx - - - - -
11 U+0080 U+07FF 2 110xxxxx 10xxxxxx - - - -
16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx - - -
21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - -
26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx -
31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

以上内容来自 维基百科 - UTF-8

关于这个编码方式,引用阮老师的总结:

UTF-8的编码规则很简单,只有二条: 1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。 2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。 From: 阮一峰的网络日志 - 字符编码笔记:ASCII,Unicode和UTF-8

于是我写了一段代码:

function strToUtf8Array(str) {
  let utf8 = [];
  for (var i = 0; i < str.length; i++) {
    let code = str.charCodeAt(i);

    if (code <= 0x007f) {
      utf8.push(code | 0b00000000);
    } else if (code < 0x07ff) {
      utf8.push((code >> 6) | 0b11000000, (code & 0x3f) | 0x80);
    } else if (code < 0xffff) {
      utf8.push(
        (code >> 12) | 0b11100000,
        ((code >> 6) & 0x3f) | 0x80,
        (code & 0x3f) | 0x80,
      );
    } else if (code < 0x1fffff) {
      utf8.push(
        (code >> 18) | 0b11110000,
        ((code >> 12) & 0x3f) | 0x80,
        ((code >> 6) & 0x3f) | 0x80,
        (code & 0x3f) | 0x80,
      );
    } else if (code < 0x3ffffff) {
      utf8.push(
        (code >> 24) | 0b11111000,
        ((code >> 18) & 0x3f) | 0x80,
        ((code >> 12) & 0x3f) | 0x80,
        ((code >> 6) & 0x3f) | 0x80,
        (code & 0x3f) | 0x80,
      );
    } else if (code < 0x7fffffff) {
      utf8.push(
        (code >> 30) | 0b11111100,
        ((code >> 24) & 0x3f) | 0x80,
        ((code >> 18) & 0x3f) | 0x80,
        ((code >> 12) & 0x3f) | 0x80,
        ((code >> 6) & 0x3f) | 0x80,
        (code & 0x3f) | 0x80,
      );
    }
  }
  return utf8;
}

function utf8ArrayToCodes(utf8) {
  let str = "",
    arr = [];
  for (var i = 0; i < utf8.length; i++) {
    var code = utf8[i],
      flag = code < 0b10000000 ? 1 : code.toString(2).indexOf(0),
      tmp = [];

    switch (flag) {
      case 6:
        tmp.unshift(utf8[i + 5].toString(2).slice(2));
      case 5:
        tmp.unshift(utf8[i + 4].toString(2).slice(2));
      case 4:
        tmp.unshift(utf8[i + 3].toString(2).slice(2));
      case 3:
        tmp.unshift(utf8[i + 2].toString(2).slice(2));
      case 2:
        tmp.unshift(utf8[i + 1].toString(2).slice(2));
      case 1:
        tmp.unshift(utf8[i].toString(2).slice(flag === 1 ? 0 : flag + 1));
    }

    arr.push(+("0b" + tmp.join("")));
    i += flag - 1;
  }
  return arr;
}

然后问题就解决了 ┑( ̄Д  ̄)┍

© 2026 香蕉引擎故障报告

🌱 Powered by Hugo with theme Dream.

关于

要怎么介绍自己呢,🤔。

很早以前是作为 Web 前端在学习的,但是工作第一年就成为了全干工程师。喜欢尝试各种东西,什么都会一点。

一直很喜欢 Ebiten 游戏引擎 ,特别简洁,用它做过一些小东西,可以查看这个分类 。另外特别推荐这个木鱼 ,是一个相对完整的小玩意儿,包含手搓的一个简单的 UI 框架;支持鼠标和键盘操作;有多语言和主题切换功能;同时支持 Web 端和客户端。它的源代码在 bin16/wooden-fish

主题

网站基于 Hugo,当前使用的是 hugo-theme-dream 主题的修改版 ,根据我的需要,做了一些对 PaperMod 的兼容。

我自己也写过主题 ,但是没有别人写的好看。

正在从我的笔记中往外搬运内容

等待更新:

  • 从《锈湖》中学了些什么东西
  • 我拿 React 写解谜游戏的经过
  • 基于 Pocketbase 的 Pocket Memos
  • 数独!