由公式字体引发的...

240 阅读4分钟

引言

工作时遇到这样的字符串𝑀𝑜𝑛𝑖𝑡𝑜𝑟𝐿𝑜𝑡𝑇𝑖𝑚𝑒2𝐷𝑢𝑒,测试让把这样的字符串做判断,如果是需要做提示,可是这玩意无论粘贴到哪里都是原样输出,很让人费解。

询问了一下出处,是在ppt里粘贴过来的,于是ppt里捣鼓了半天,发现不是字体的问题,反倒和ppt里插入公式的字体有点像,又尝试插入一样的字符,发现样子一毛一样,因此断定是测试给的是公式字体。

gif-2022-05-02.gif

在“低调务实优秀中国好青年”群里问了之后,@windling同学给出了答案:追本溯源:字符串及编码 (qq.com),我才刹那明白,原来是编码问题。

js的字符编码

在计算机中由二进制来保存数据,所有数据由0和1组成,每个0或者1叫1bit—位,1byte(字节)=8bits1KB = 1024 bytes1MB(兆) = 1024KB1GB = 1024MB1TB = 1024GB

国际标准组织为了统一世界各国的字符,把它们都放在了张表里,为每个字符都分配一个数字,这就是Unicode码表,这样,每个字符都对应了唯一的数字,这些数字又叫做码点(Code Point)。

在javascript中使用UCS-2编码(后整合进了UTF-16,故和UTF-16编码方式相同),即16个bit位两个字节来表示一个码点,这16个bit位叫做代码单元(码元,code unit)。那么这样两个字节能够表示的最大数是65535(16进制0xFFFF),如果超过的话,则使用两个码元表示一个字符。

js的字符串操作

字符转码点/码元

在js中应该如何获得这些数字呢?

// str.charCodeAt(pos) 获得码元
// str.codePointAt(pos) 获得码点 pos表示str中字符的位置 不写默认第0位
'abc'.charCodeAt(1) // 98
'123'.charCodeAt() // 49
'abc'.codePointAt(1) // 98
'123'.codePointAt() // 49
'𠮷'.charCodeAt() // 55362
'𠮷'.codePointAt() // 134071

可以看到,在65535以内码元和码点的值是一样的。遇到超过65535外的字符charCodeAt就不能正确转换了,它实际上用了两个码元来表示,而charCodeAt只返回了一个码元,codePointAt则能正确表示。而事实上codePointAt是ES6为了对Unicode字符的支持增加的,兼容了charCodeAt,后文提到的fromCodePoint也是一样。

码元/码点转字符

那么对应的,也应该有码元码点转字符的方法:

// String.fromCharCode(num1,num2,...) 码元获得字符
// String.fromCodePoint(num1, num2,...) 码点获得字符
String.fromCharCode(48, 49, 50) // 012
String.fromCodePoint(97, 98, 99) // abc

判断

好了,有了以上知识,那么判断这样的字符也很简单:

const str = '𝑀𝑜𝑛𝑖𝑡𝑜𝑟𝐿𝑜𝑡𝑇𝑖𝑚𝑒2𝐷𝑢𝑒';
for (let s of str) {
  if (s.codePointAt() > 0xffff) {
    console.log(`${s} is a special character`);
  } else {
    console.log(`${s} is a normal character`);
  }
}

公式字符的互转

这样工作似乎完成了?好奇的我又想了,如果想转换为普通字符呢?

首先我需要知道它们的码点值,以 𝑀 为例,'𝑀'.codePointAt(0) 拿到了它的码点是119872,那么一个普通字母M的码点值是77,相减得到一个差值,那么所有的大写字母不就都可以互相转换了吗?相应的小写字母也是如此。

// 转普通字符
const str = '𝑀𝑜𝑛𝑖𝑡𝑜𝑟𝐿𝑜𝑡𝑇𝑖𝑚𝑒2𝐷𝑢𝑒';
function toNormalStr(str) {
  const result = [];
  for (const s of str) {
    const t = s.codePointAt();
    if (t >= 0x1d434 && t <= 0x1d44d) {
      result.push(String.fromCharCode(t - 119795));
    } else if (t >= 0x1d44e && t <= 0x1d467) {
      result.push(String.fromCharCode(t - 119789));
    } else {
      result.push(s);
    }
  }
  return result.join('');
}
console.log(toNormalStr(str)); // MonitorLotTime2Due
// 转公式字符
const str = 'MonitorLotTime2Due';
function toSpecialStr(str) {
  const result = [];
  for (const s of str) {
    const t = s.charCodeAt();
    if (t >= 65 && t <= 90) {
      result.push(String.fromCodePoint(119795 + t));
    } else if (t >= 97 && t <= 122) {
      result.push(String.fromCodePoint(119789 + t));
    } else {
      result.push(s);
    }
  }
  return result.join('');
}
console.log(toSpecialStr(str)); // 𝑀𝑜𝑛𝑖𝑡𝑜𝑟𝐿𝑜𝑡𝑇𝑖𝑚𝑒2𝐷𝑢𝑒

至于其他的一些数学符号就不搞了,也都是找差值计算。。

另外去找Unicode 14.0 字符代码表里的Mathematical Alphanumeric Symbols 数学字母数字符号,可以找到更多,在js里使用'𝑀'.codePointAt(0).toString(16)得到16进制的值,再去对应找字符。

其他的一些?

知道了16进制转字符? 如:"\u{1D4A5}\u{1D4A6}\u{1D4A7}\u{1D4A8}".toString() 得到 '𝒥𝒦𝒧𝒨'

知道了16进制转码点? 如:'\u{1D44e}'.codePointAt()获得119886

知道了码点转16进制? 如:Number(119863).toString(16) 获得 '1d437'

又想到:js中使用UTF-16的编码方式,那么一个字母占2个字节?是的。这和我们认知的一个英文字母数字占1个字节,一个中文字符占2个字节不太一样。

参考

追本溯源:字符串及编码 (qq.com)

ES6字符串操作讲解

Unicode与JavaScript详解 - 阮一峰的网络日志 (ruanyifeng.com)

String - JavaScript | MDN (mozilla.org)

没了?这就没了?emm...